diff options
Diffstat (limited to 'StoneIsland/platforms/android')
124 files changed, 6640 insertions, 1316 deletions
diff --git a/StoneIsland/platforms/android/AndroidManifest.xml b/StoneIsland/platforms/android/AndroidManifest.xml index da0336cf..b8feb984 100755 --- a/StoneIsland/platforms/android/AndroidManifest.xml +++ b/StoneIsland/platforms/android/AndroidManifest.xml @@ -1,5 +1,5 @@ <?xml version='1.0' encoding='utf-8'?> -<manifest android:hardwareAccelerated="true" android:versionCode="700" android:versionName="0.7.0" package="us.okfoc.stoneisland" xmlns:android="http://schemas.android.com/apk/res/android"> +<manifest android:hardwareAccelerated="true" android:versionCode="801" android:versionName="0.8.1" package="us.okfoc.stoneisland" xmlns:android="http://schemas.android.com/apk/res/android"> <supports-screens android:anyDensity="true" android:largeScreens="true" android:normalScreens="true" android:resizeable="true" android:smallScreens="true" android:xlargeScreens="true" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> @@ -43,6 +43,25 @@ <category android:name="us.okfoc.stoneisland" /> </intent-filter> </receiver> + <activity android:exported="true" android:name="com.adobe.phonegap.push.PushHandlerActivity" android:permission="${applicationId}.permission.PushHandlerActivity" /> + <receiver android:name="com.adobe.phonegap.push.BackgroundActionButtonHandler" /> + <receiver android:exported="true" android:name="com.google.android.gms.gcm.GcmReceiver" android:permission="com.google.android.c2dm.permission.SEND"> + <intent-filter> + <action android:name="com.google.android.c2dm.intent.RECEIVE" /> + <category android:name="${applicationId}" /> + </intent-filter> + </receiver> + <service android:exported="false" android:name="com.adobe.phonegap.push.GCMIntentService"> + <intent-filter> + <action android:name="com.google.android.c2dm.intent.RECEIVE" /> + </intent-filter> + </service> + <service android:exported="false" android:name="com.adobe.phonegap.push.PushInstanceIDListenerService"> + <intent-filter> + <action android:name="com.google.android.gms.iid.InstanceID" /> + </intent-filter> + </service> + <service android:exported="false" android:name="com.adobe.phonegap.push.RegistrationIntentService" /> </application> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="22" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> @@ -57,4 +76,8 @@ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-feature android:name="android.hardware.location.gps" /> + <uses-permission android:name="${applicationId}.permission.C2D_MESSAGE" /> + <uses-permission android:name="${applicationId}.permission.PushHandlerActivity" /> + <permission android:name="${applicationId}.permission.C2D_MESSAGE" android:protectionLevel="signature" /> + <permission android:name="${applicationId}.permission.PushHandlerActivity" android:protectionLevel="signature" /> </manifest> diff --git a/StoneIsland/platforms/android/CordovaLib/build.gradle b/StoneIsland/platforms/android/CordovaLib/build.gradle index 2565633f..b580e13f 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/build.gradle +++ b/StoneIsland/platforms/android/CordovaLib/build.gradle @@ -21,28 +21,22 @@ buildscript { repositories { mavenCentral() + jcenter(); } - // Switch the Android Gradle plugin version requirement depending on the - // installed version of Gradle. This dependency is documented at - // http://tools.android.com/tech-docs/new-build-system/version-compatibility - // and https://issues.apache.org/jira/browse/CB-8143 - if (gradle.gradleVersion >= "2.2") { - dependencies { - classpath 'com.android.tools.build:gradle:1.0.0+' - } - } else if (gradle.gradleVersion >= "2.1") { - dependencies { - classpath 'com.android.tools.build:gradle:0.14.0+' - } - } else { - dependencies { - classpath 'com.android.tools.build:gradle:0.12.0+' - } + dependencies { + classpath 'com.android.tools.build:gradle:2.2.1' } + } -apply plugin: 'android-library' +apply plugin: 'com.android.library' + +ext { + apply from: 'cordova.gradle' + cdvCompileSdkVersion = privateHelpers.getProjectTarget() + cdvBuildToolsVersion = privateHelpers.findLatestInstalledBuildTools() +} android { compileSdkVersion cdvCompileSdkVersion diff --git a/StoneIsland/platforms/android/CordovaLib/cordova.gradle b/StoneIsland/platforms/android/CordovaLib/cordova.gradle index 6e89c4c2..21a01bb5 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/cordova.gradle +++ b/StoneIsland/platforms/android/CordovaLib/cordova.gradle @@ -61,7 +61,7 @@ String doFindLatestInstalledBuildTools(String minBuildToolsVersion) { highestBuildToolsVersion } else { throw new RuntimeException( - "No installed build tools found. Please install the Android build tools version " + + "No installed build tools found. Install the Android build tools version " + minBuildToolsVersion + " or higher.") } } @@ -125,7 +125,15 @@ def doExtractIntFromManifest(name) { def pattern = Pattern.compile(name + "=\"(\\d+)\"") def matcher = pattern.matcher(manifestFile.getText()) matcher.find() - return Integer.parseInt(matcher.group(1)) + return new BigInteger(matcher.group(1)) +} + +def doExtractStringFromManifest(name) { + def manifestFile = file(android.sourceSets.main.manifest.srcFile) + def pattern = Pattern.compile(name + "=\"(\\S+)\"") + def matcher = pattern.matcher(manifestFile.getText()) + matcher.find() + return matcher.group(1) } def doPromptForPassword(msg) { @@ -179,6 +187,7 @@ ext { privateHelpers.getProjectTarget = { doGetProjectTarget() } privateHelpers.findLatestInstalledBuildTools = { doFindLatestInstalledBuildTools('19.1.0') } privateHelpers.extractIntFromManifest = { name -> doExtractIntFromManifest(name) } + privateHelpers.extractStringFromManifest = { name -> doExtractStringFromManifest(name) } privateHelpers.promptForPassword = { msg -> doPromptForPassword(msg) } privateHelpers.ensureValueExists = { filePath, props, key -> doEnsureValueExists(filePath, props, key) } diff --git a/StoneIsland/platforms/android/CordovaLib/project.properties b/StoneIsland/platforms/android/CordovaLib/project.properties index 40ae82c1..df3c73c4 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/project.properties +++ b/StoneIsland/platforms/android/CordovaLib/project.properties @@ -10,7 +10,7 @@ # Indicates whether an apk should be generated for each density. split.density=false # Project target. -target=android-22 +target=android-25 apk-configurations= renderscript.opt.level=O0 android.library=true diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/AuthenticationToken.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/AuthenticationToken.java index d3a231a0..d3a231a0 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/AuthenticationToken.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/AuthenticationToken.java diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CallbackContext.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CallbackContext.java index 446c37d9..43363869 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CallbackContext.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CallbackContext.java @@ -20,8 +20,6 @@ package org.apache.cordova; import org.json.JSONArray; -import android.util.Log; - import org.apache.cordova.CordovaWebView; import org.apache.cordova.PluginResult; import org.json.JSONObject; @@ -31,22 +29,22 @@ public class CallbackContext { private String callbackId; private CordovaWebView webView; - private boolean finished; + protected boolean finished; private int changingThreads; public CallbackContext(String callbackId, CordovaWebView webView) { this.callbackId = callbackId; this.webView = webView; } - + public boolean isFinished() { return finished; } - + public boolean isChangingThreads() { return changingThreads > 0; } - + public String getCallbackId() { return callbackId; } @@ -54,7 +52,7 @@ public class CallbackContext { public void sendPluginResult(PluginResult pluginResult) { synchronized (this) { if (finished) { - Log.w(LOG_TAG, "Attempted to send a second callback for ID: " + callbackId + "\nResult was: " + pluginResult.getMessage()); + LOG.w(LOG_TAG, "Attempted to send a second callback for ID: " + callbackId + "\nResult was: " + pluginResult.getMessage()); return; } else { finished = !pluginResult.getKeepCallback(); @@ -98,7 +96,7 @@ public class CallbackContext { public void success(byte[] message) { sendPluginResult(new PluginResult(PluginResult.Status.OK, message)); } - + /** * Helper for success callbacks that just returns the Status.OK by default * diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CallbackMap.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CallbackMap.java new file mode 100644 index 00000000..050daa01 --- /dev/null +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CallbackMap.java @@ -0,0 +1,65 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.cordova; + +import android.util.Pair; +import android.util.SparseArray; + +/** + * Provides a collection that maps unique request codes to CordovaPlugins and Integers. + * Used to ensure that when plugins make requests for runtime permissions, those requests do not + * collide with requests from other plugins that use the same request code value. + */ +public class CallbackMap { + private int currentCallbackId = 0; + private SparseArray<Pair<CordovaPlugin, Integer>> callbacks; + + public CallbackMap() { + this.callbacks = new SparseArray<Pair<CordovaPlugin, Integer>>(); + } + + /** + * Stores a CordovaPlugin and request code and returns a new unique request code to use + * in a permission request. + * + * @param receiver The plugin that is making the request + * @param requestCode The original request code used by the plugin + * @return A unique request code that can be used to retrieve this callback + * with getAndRemoveCallback() + */ + public synchronized int registerCallback(CordovaPlugin receiver, int requestCode) { + int mappedId = this.currentCallbackId++; + callbacks.put(mappedId, new Pair<CordovaPlugin, Integer>(receiver, requestCode)); + return mappedId; + } + + /** + * Retrieves and removes a callback stored in the map using the mapped request code + * obtained from registerCallback() + * + * @param mappedId The request code obtained from registerCallback() + * @return The CordovaPlugin and orignal request code that correspond to the + * given mappedCode + */ + public synchronized Pair<CordovaPlugin, Integer> getAndRemoveCallback(int mappedId) { + Pair<CordovaPlugin, Integer> callback = callbacks.get(mappedId); + callbacks.remove(mappedId); + return callback; + } +} diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/Config.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/Config.java index 048960bf..07397959 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/Config.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/Config.java @@ -22,7 +22,6 @@ package org.apache.cordova; import java.util.List; import android.app.Activity; -import android.util.Log; @Deprecated // Use Whitelist, CordovaPrefences, etc. directly. public class Config { @@ -61,7 +60,7 @@ public class Config { public static List<PluginEntry> getPluginEntries() { return parser.getPluginEntries(); } - + public static CordovaPreferences getPreferences() { return parser.getPreferences(); } diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/ConfigXmlParser.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/ConfigXmlParser.java index 01a97f2d..01a97f2d 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/ConfigXmlParser.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/ConfigXmlParser.java diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaActivity.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaActivity.java index 5c3bc508..85eeb53a 100755 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaActivity.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaActivity.java @@ -26,6 +26,7 @@ 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; @@ -33,7 +34,6 @@ import android.graphics.Color; import android.media.AudioManager; import android.os.Build; import android.os.Bundle; -import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -49,7 +49,7 @@ import android.widget.FrameLayout; * html file that contains the application. * * As an example: - * + * * <pre> * package org.apache.cordova.examples; * @@ -66,8 +66,8 @@ import android.widget.FrameLayout; * } * } * </pre> - * - * Cordova xml configuration: Cordova uses a configuration file at + * + * 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 @@ -103,31 +103,31 @@ public class CordovaActivity extends Activity { */ @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()"); - // need to activate preferences before super.onCreate to avoid "requestFeature() must be called before adding content" exception - loadConfig(); - if(!preferences.getBoolean("ShowTitle", false)) - { + 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."); + 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)) - { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) - { + 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 ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) && !preferences.getBoolean("FullscreenNotImmersive", false)) { immersiveMode = true; - } - else - { + } else { getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, - WindowManager.LayoutParams.FLAG_FULLSCREEN); + WindowManager.LayoutParams.FLAG_FULLSCREEN); } } else { getWindow().setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN, @@ -137,12 +137,11 @@ public class CordovaActivity extends Activity { super.onCreate(savedInstanceState); cordovaInterface = makeCordovaInterface(); - if(savedInstanceState != null) - { + if (savedInstanceState != null) { cordovaInterface.restoreInstanceState(savedInstanceState); } } - + protected void init() { appView = makeWebView(); createViews(); @@ -181,9 +180,14 @@ public class CordovaActivity extends Activity { setContentView(appView.getView()); if (preferences.contains("BackgroundColor")) { - int backgroundColor = preferences.getInteger("BackgroundColor", Color.BLACK); - // Background of activity: - appView.getView().setBackgroundColor(backgroundColor); + try { + int backgroundColor = preferences.getInteger("BackgroundColor", Color.BLACK); + // Background of activity: + appView.getView().setBackgroundColor(backgroundColor); + } + catch (NumberFormatException e){ + e.printStackTrace(); + } } appView.getView().requestFocusFromTouch(); @@ -191,7 +195,7 @@ public class CordovaActivity extends Activity { /** * Construct the default web view object. - * + * <p/> * Override this to customize the webview that is used. */ protected CordovaWebView makeWebView() { @@ -244,13 +248,13 @@ public class CordovaActivity extends Activity { /** * 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); + this.appView.onNewIntent(intent); } /** @@ -260,7 +264,7 @@ public class CordovaActivity extends Activity { protected void onResume() { super.onResume(); LOG.d(TAG, "Resumed the activity."); - + if (this.appView == null) { return; } @@ -320,16 +324,17 @@ public class CordovaActivity extends Activity { 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; + | 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. @@ -341,10 +346,10 @@ public class CordovaActivity extends Activity { * 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"). + * @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) { @@ -357,9 +362,9 @@ public class CordovaActivity extends Activity { * 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. + * @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; @@ -448,9 +453,9 @@ public class CordovaActivity extends Activity { /** * Called when a message is sent to plugin. * - * @param id The message id - * @param data The message data - * @return Object or null + * @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)) { @@ -466,8 +471,7 @@ public class CordovaActivity extends Activity { return null; } - protected void onSaveInstanceState(Bundle outState) - { + protected void onSaveInstanceState(Bundle outState) { cordovaInterface.onSaveInstanceState(outState); super.onSaveInstanceState(outState); } @@ -475,7 +479,7 @@ public class CordovaActivity extends Activity { /** * Called by the system when the device configuration changes while your activity is running. * - * @param newConfig The new device configuration + * @param newConfig The new device configuration */ @Override public void onConfigurationChanged(Configuration newConfig) { @@ -488,4 +492,27 @@ public class CordovaActivity extends Activity { 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/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaArgs.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaArgs.java index d40d26eb..d40d26eb 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaArgs.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaArgs.java diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaBridge.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaBridge.java index 7bc4a552..9459a113 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaBridge.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaBridge.java @@ -23,8 +23,6 @@ import java.security.SecureRandom; import org.json.JSONArray; import org.json.JSONException; -import android.util.Log; - /** * Contains APIs that the JS can call. All functions in here should also have * an equivalent entry in CordovaChromeClient.java, and be added to @@ -87,15 +85,15 @@ public class CordovaBridge { 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."); + LOG.d(LOG_TAG, action + " call made before bridge was enabled."); } else { - Log.d(LOG_TAG, "Ignoring " + action + " from previous page load."); + 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!"); + LOG.e(LOG_TAG, "Bridge access attempt with wrong secret token, possibly from malicious code. Disabling exec() bridge!"); clearBridgeSecret(); throw new IllegalAccessException(); } @@ -120,7 +118,7 @@ public class CordovaBridge { public void reset() { jsMessageQueue.reset(); - clearBridgeSecret(); + clearBridgeSecret(); } public String promptOnJsPrompt(String origin, String message, String defaultValue) { @@ -141,7 +139,7 @@ public class CordovaBridge { } return ""; } - // Sets the native->JS bridge mode. + // Sets the native->JS bridge mode. else if (defaultValue != null && defaultValue.startsWith("gap_bridge_mode:")) { try { int bridgeSecret = Integer.parseInt(defaultValue.substring(16)); @@ -153,7 +151,7 @@ public class CordovaBridge { } return ""; } - // Polling for JavaScript messages + // Polling for JavaScript messages else if (defaultValue != null && defaultValue.startsWith("gap_poll:")) { int bridgeSecret = Integer.parseInt(defaultValue.substring(9)); try { @@ -175,7 +173,7 @@ public class CordovaBridge { int secret = generateBridgeSecret(); return ""+secret; } else { - Log.e(LOG_TAG, "gap_init called from restricted origin: " + origin); + LOG.e(LOG_TAG, "gap_init called from restricted origin: " + origin); } return ""; } diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaClientCertRequest.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaClientCertRequest.java index 5dd0ecae..5dd0ecae 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaClientCertRequest.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaClientCertRequest.java diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaDialogsHelper.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaDialogsHelper.java index a219c992..a219c992 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaDialogsHelper.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaDialogsHelper.java diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaHttpAuthHandler.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaHttpAuthHandler.java index 724381e2..724381e2 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaHttpAuthHandler.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaHttpAuthHandler.java diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaInterface.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaInterface.java index 59ed4864..3b8468f3 100755 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaInterface.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaInterface.java @@ -69,4 +69,20 @@ public interface CordovaInterface { * 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/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaInterfaceImpl.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaInterfaceImpl.java index e35a181d..71dcb782 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaInterfaceImpl.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaInterfaceImpl.java @@ -21,8 +21,13 @@ package org.apache.cordova; import android.app.Activity; import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Build; import android.os.Bundle; -import android.util.Log; +import android.util.Pair; + +import org.json.JSONException; +import org.json.JSONObject; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -37,9 +42,12 @@ public class CordovaInterfaceImpl implements CordovaInterface { 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()); @@ -48,6 +56,7 @@ public class CordovaInterfaceImpl implements CordovaInterface { public CordovaInterfaceImpl(Activity activity, ExecutorService threadPool) { this.activity = activity; this.threadPool = threadPool; + this.permissionResultCallbacks = new CallbackMap(); } @Override @@ -89,12 +98,31 @@ public class CordovaInterfaceImpl implements CordovaInterface { } /** - * Dispatches any pending onActivityResult callbacks. + * 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)); + } + } + } } @@ -109,18 +137,22 @@ public class CordovaInterfaceImpl implements CordovaInterface { 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"); + 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!": ".")); + LOG.w(TAG, "Got an activity result, but no plugin was registered to receive it" + (savedResult != null ? " yet!" : ".")); return false; } @@ -141,6 +173,10 @@ public class CordovaInterfaceImpl implements CordovaInterface { String serviceName = activityResultCallback.getServiceName(); outState.putString("callbackService", serviceName); } + if(pluginManager != null){ + outState.putBundle("plugin", pluginManager.onSaveInstanceState()); + } + } /** @@ -148,6 +184,8 @@ public class CordovaInterfaceImpl implements CordovaInterface { */ public void restoreInstanceState(Bundle savedInstanceState) { initCallbackService = savedInstanceState.getString("callbackService"); + savedPluginState = savedInstanceState.getBundle("plugin"); + activityWasDestroyed = true; } private static class ActivityResultHolder { @@ -161,4 +199,43 @@ public class CordovaInterfaceImpl implements CordovaInterface { this.intent = intent; } } + + /** + * Called by the system when the user grants permissions + * + * @param requestCode + * @param permissions + * @param grantResults + */ + public void onRequestPermissionResult(int requestCode, String[] permissions, + int[] grantResults) throws JSONException { + Pair<CordovaPlugin, Integer> callback = permissionResultCallbacks.getAndRemoveCallback(requestCode); + if(callback != null) { + callback.first.onRequestPermissionResult(callback.second, permissions, grantResults); + } + } + + public void requestPermission(CordovaPlugin plugin, int requestCode, String permission) { + String[] permissions = new String [1]; + permissions[0] = permission; + requestPermissions(plugin, requestCode, permissions); + } + + 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/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaPlugin.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaPlugin.java index 7cf8528f..41af1db7 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaPlugin.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaPlugin.java @@ -26,8 +26,11 @@ 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; @@ -75,7 +78,7 @@ public class CordovaPlugin { public String getServiceName() { return serviceName; } - + /** * Executes the request. * @@ -173,6 +176,29 @@ public class CordovaPlugin { } /** + * 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 @@ -321,7 +347,7 @@ public class CordovaPlugin { */ public void onReset() { } - + /** * Called when the system received an HTTP authentication request. Plugin can use * the supplied HttpAuthHandler to process this auth challenge. @@ -330,14 +356,14 @@ public class CordovaPlugin { * @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. @@ -359,4 +385,38 @@ public class CordovaPlugin { */ 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/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaPreferences.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaPreferences.java index 4dbc93e6..4dbc93e6 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaPreferences.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaPreferences.java diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaResourceApi.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaResourceApi.java index e725e255..e725e255 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaResourceApi.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaResourceApi.java diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaWebView.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaWebView.java index ba58f9a0..2eebd0d3 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaWebView.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaWebView.java @@ -31,7 +31,7 @@ import android.webkit.WebChromeClient.CustomViewCallback; * are not expected to implement it. */ public interface CordovaWebView { - public static final String CORDOVA_VERSION = "4.1.1"; + public static final String CORDOVA_VERSION = "6.1.2"; void init(CordovaInterface cordova, List<PluginEntry> pluginEntries, CordovaPreferences preferences); diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaWebViewEngine.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaWebViewEngine.java index dd84ab11..c8e5a55d 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaWebViewEngine.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaWebViewEngine.java @@ -20,9 +20,10 @@ package org.apache.cordova; import android.view.KeyEvent; import android.view.View; +import android.webkit.ValueCallback; /** - * Interfcae for all Cordova engines. + * 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 */ @@ -58,6 +59,9 @@ public interface CordovaWebViewEngine { /** Clean up all resources associated with the WebView. */ void destroy(); + /** Add the evaulate Javascript method **/ + void evaluateJavascript(String js, ValueCallback<String> callback); + /** * Used to retrieve the associated CordovaWebView given a View without knowing the type of Engine. * E.g. ((CordovaWebView.EngineView)activity.findViewById(android.R.id.webView)).getCordovaWebView(); diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaWebViewImpl.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaWebViewImpl.java index 06da55e1..85a0b5f5 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaWebViewImpl.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CordovaWebViewImpl.java @@ -21,7 +21,6 @@ package org.apache.cordova; import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.View; @@ -135,6 +134,7 @@ public class CordovaWebViewImpl implements CordovaWebView { if (recreatePlugins) { // Don't re-initialize on first load. if (loadedUrl != null) { + appPlugin = null; pluginManager.init(); } loadedUrl = url; @@ -244,7 +244,7 @@ public class CordovaWebViewImpl implements CordovaWebView { @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"); + LOG.d(TAG, "showing Custom View"); // if a view already exists then immediately terminate the new one if (mCustomView != null) { callback.onCustomViewHidden(); @@ -275,7 +275,7 @@ public class CordovaWebViewImpl implements CordovaWebView { 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"); + LOG.d(TAG, "Hiding Custom View"); // Hide the custom view. mCustomView.setVisibility(View.GONE); @@ -354,6 +354,7 @@ public class CordovaWebViewImpl implements CordovaWebView { 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); @@ -445,7 +446,10 @@ public class CordovaWebViewImpl implements CordovaWebView { // Resume JavaScript timers. This affects all webviews within the app! engine.setPaused(false); this.pluginManager.onResume(keepRunning); - // To be the same as other platforms, fire this event only when resumed after a "pause". + + // 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"); } diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CoreAndroid.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CoreAndroid.java index 000717a2..e384f8d7 100755 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CoreAndroid.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/CoreAndroid.java @@ -19,10 +19,6 @@ package org.apache.cordova; -import org.apache.cordova.CallbackContext; -import org.apache.cordova.CordovaPlugin; -import org.apache.cordova.LOG; -import org.apache.cordova.PluginResult; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -34,17 +30,20 @@ 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. */ -class CoreAndroid extends CordovaPlugin { +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. @@ -112,7 +111,13 @@ class CoreAndroid extends CordovaPlugin { this.exitApp(); } else if (action.equals("messageChannel")) { - messageChannel = callbackContext; + synchronized(messageChannelLock) { + messageChannel = callbackContext; + if (pendingResume != null) { + sendEventMessage(pendingResume); + pendingResume = null; + } + } return true; } @@ -248,6 +253,9 @@ class CoreAndroid extends CordovaPlugin { else if (button.equals("volumedown")) { webView.setButtonPlumbedToJs(KeyEvent.KEYCODE_VOLUME_DOWN, override); } + else if (button.equals("menubutton")) { + webView.setButtonPlumbedToJs(KeyEvent.KEYCODE_MENU, override); + } } /** @@ -313,10 +321,13 @@ class CoreAndroid extends CordovaPlugin { } catch (JSONException e) { LOG.e(TAG, "Failed to create event message", e); } - PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, obj); - pluginResult.setKeepCallback(true); + sendEventMessage(new PluginResult(PluginResult.Status.OK, obj)); + } + + private void sendEventMessage(PluginResult payload) { + payload.setKeepCallback(true); if (messageChannel != null) { - messageChannel.sendPluginResult(pluginResult); + messageChannel.sendPluginResult(payload); } } @@ -328,4 +339,52 @@ class CoreAndroid extends CordovaPlugin { { 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/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/ExposedJsApi.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/ExposedJsApi.java index acc65c6f..acc65c6f 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/ExposedJsApi.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/ExposedJsApi.java diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/ICordovaClientCertRequest.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/ICordovaClientCertRequest.java index 455d2f9d..455d2f9d 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/ICordovaClientCertRequest.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/ICordovaClientCertRequest.java diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/ICordovaCookieManager.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/ICordovaCookieManager.java index e776194f..e776194f 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/ICordovaCookieManager.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/ICordovaCookieManager.java diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/ICordovaHttpAuthHandler.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/ICordovaHttpAuthHandler.java index c55818ac..c55818ac 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/ICordovaHttpAuthHandler.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/ICordovaHttpAuthHandler.java diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/LOG.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/LOG.java index 3dd1a754..9fe7a7df 100755 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/LOG.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/LOG.java @@ -158,6 +158,16 @@ public class LOG { * 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 */ diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/NativeToJsMessageQueue.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/NativeToJsMessageQueue.java index a05e8b81..61d04f17 100755 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/NativeToJsMessageQueue.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/NativeToJsMessageQueue.java @@ -21,8 +21,6 @@ package org.apache.cordova; import java.util.ArrayList; import java.util.LinkedList; -import android.util.Log; - /** * Holds the list of messages to be sent to the WebView. */ @@ -42,13 +40,13 @@ public class NativeToJsMessageQueue { // 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. + * 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. */ @@ -58,7 +56,7 @@ public class NativeToJsMessageQueue { * The array of listeners that can be used to send messages to JS. */ private ArrayList<BridgeMode> bridgeModes = new ArrayList<BridgeMode>(); - + /** * When null, the bridge is disabled. This occurs during page transitions. * When disabled, all callbacks are dropped since they are assumed to be @@ -83,11 +81,11 @@ public class NativeToJsMessageQueue { */ public void setBridgeMode(int value) { if (value < -1 || value >= bridgeModes.size()) { - Log.d(LOG_TAG, "Invalid NativeToJsBridgeMode: " + value); + 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())); + LOG.d(LOG_TAG, "Set native->JS mode to " + (newMode == null ? "null" : newMode.getClass().getSimpleName())); synchronized (this) { activeBridgeMode = newMode; if (newMode != null) { @@ -100,7 +98,7 @@ public class NativeToJsMessageQueue { } } } - + /** * Clears all messages and resets to the default bridge mode. */ @@ -114,16 +112,16 @@ public class NativeToJsMessageQueue { private int calculatePackedMessageLength(JsMessage message) { int messageLen = message.calculateEncodedLength(); String messageLenStr = String.valueOf(messageLen); - return messageLenStr.length() + messageLen + 1; + 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. @@ -154,7 +152,7 @@ public class NativeToJsMessageQueue { JsMessage message = queue.removeFirst(); packMessage(message, sb); } - + if (!queue.isEmpty()) { // Attach a char to indicate that there are more messages pending. sb.append('*'); @@ -163,7 +161,7 @@ public class NativeToJsMessageQueue { return ret; } } - + /** * Same as popAndEncode(), except encodes in a form that can be executed as JS. */ @@ -185,7 +183,7 @@ public class NativeToJsMessageQueue { } 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 + // 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(); @@ -206,7 +204,7 @@ public class NativeToJsMessageQueue { String ret = sb.toString(); return ret; } - } + } /** * Add a JavaScript statement to the list. @@ -220,7 +218,7 @@ public class NativeToJsMessageQueue { */ public void addPluginResult(PluginResult result, String callbackId) { if (callbackId == null) { - Log.e(LOG_TAG, "Got plugin result with no callbackId", new Throwable()); + 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 @@ -243,7 +241,7 @@ public class NativeToJsMessageQueue { private void enqueueMessage(JsMessage message) { synchronized (this) { if (activeBridgeMode == null) { - Log.d(LOG_TAG, "Dropping Native->JS message due to disabled bridge"); + LOG.d(LOG_TAG, "Dropping Native->JS message due to disabled bridge"); return; } queue.add(message); @@ -257,7 +255,7 @@ public class NativeToJsMessageQueue { 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()); + LOG.e(LOG_TAG, "nested call to setPaused detected.", new Throwable()); } paused = value; if (!value) { @@ -265,7 +263,7 @@ public class NativeToJsMessageQueue { if (!queue.isEmpty() && activeBridgeMode != null) { activeBridgeMode.onNativeToJsMessageAvailable(this); } - } + } } } @@ -351,6 +349,31 @@ public class NativeToJsMessageQueue { } } + /** 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; @@ -368,7 +391,7 @@ public class NativeToJsMessageQueue { jsPayloadOrCallbackId = callbackId; this.pluginResult = pluginResult; } - + static int calculateEncodedLengthHelper(PluginResult pluginResult) { switch (pluginResult.getMessageType()) { case PluginResult.MESSAGE_TYPE_BOOLEAN: // f or t @@ -395,7 +418,7 @@ public class NativeToJsMessageQueue { return pluginResult.getMessage().length(); } } - + int calculateEncodedLength() { if (pluginResult == null) { return jsPayloadOrCallbackId.length() + 1; @@ -424,7 +447,7 @@ public class NativeToJsMessageQueue { case PluginResult.MESSAGE_TYPE_BINARYSTRING: // S sb.append('S'); sb.append(pluginResult.getMessage()); - break; + break; case PluginResult.MESSAGE_TYPE_ARRAYBUFFER: // A sb.append('A'); sb.append(pluginResult.getMessage()); @@ -443,7 +466,7 @@ public class NativeToJsMessageQueue { sb.append(pluginResult.getMessage()); // [ or { } } - + void encodeAsMessage(StringBuilder sb) { if (pluginResult == null) { sb.append('J') diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/PluginManager.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/PluginManager.java index a541e770..c9576a6c 100755 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/PluginManager.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/PluginManager.java @@ -26,8 +26,8 @@ import org.json.JSONException; import android.content.Intent; import android.content.res.Configuration; import android.net.Uri; +import android.os.Bundle; import android.os.Debug; -import android.util.Log; /** * PluginManager is exposed to JavaScript in the Cordova WebView. @@ -47,6 +47,8 @@ public class PluginManager { private final CordovaWebView app; private boolean isInitialized; + private CordovaPlugin permissionRequester; + public PluginManager(CordovaWebView cordovaWebView, CordovaInterface cordova, Collection<PluginEntry> pluginEntries) { this.ctx = cordova; this.app = cordovaWebView; @@ -119,7 +121,7 @@ public class PluginManager { 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); + LOG.d(TAG, "exec() call to unknown plugin: " + service); PluginResult cr = new PluginResult(PluginResult.Status.CLASS_NOT_FOUND_EXCEPTION); app.sendPluginResult(cr, callbackId); return; @@ -131,7 +133,7 @@ public class PluginManager { 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()."); + 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); @@ -141,7 +143,7 @@ public class PluginManager { PluginResult cr = new PluginResult(PluginResult.Status.JSON_EXCEPTION); callbackContext.sendPluginResult(cr); } catch (Exception e) { - Log.e(TAG, "Uncaught exception from plugin", e); + LOG.e(TAG, "Uncaught exception from plugin", e); callbackContext.error(e.getMessage()); } } @@ -219,9 +221,9 @@ public class PluginManager { * @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()) { @@ -231,7 +233,7 @@ public class PluginManager { } return false; } - + /** * Called when he system received an SSL client certificate request. Plugin can use * the supplied ClientCertRequest to process this certificate challenge. @@ -508,4 +510,17 @@ public class PluginManager { } } } + + 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/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/PluginResult.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/PluginResult.java index 2b3ac72e..2b3ac72e 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/PluginResult.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/PluginResult.java diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/ResumeCallback.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/ResumeCallback.java new file mode 100644 index 00000000..49a43b5d --- /dev/null +++ b/StoneIsland/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<PluginResult> result = new ArrayList<PluginResult>(); + result.add(eventResult); + result.add(pluginResult); + + CoreAndroid appPlugin = (CoreAndroid) pluginManager.getPlugin(CoreAndroid.PLUGIN_NAME); + appPlugin.sendResumeEvent(new PluginResult(PluginResult.Status.OK, result)); + } +} diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/Whitelist.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/Whitelist.java index d0f823c3..d0f823c3 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/Whitelist.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/Whitelist.java diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemCookieManager.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemCookieManager.java index ae55dfee..acf795fa 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemCookieManager.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemCookieManager.java @@ -19,6 +19,7 @@ package org.apache.cordova.engine; +import android.annotation.TargetApi; import android.os.Build; import android.webkit.CookieManager; import android.webkit.WebView; @@ -30,10 +31,15 @@ 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); } diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemWebChromeClient.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemWebChromeClient.java index 3b5866c3..3ea5e576 100755 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemWebChromeClient.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemWebChromeClient.java @@ -21,11 +21,11 @@ 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.util.Log; import android.view.Gravity; import android.view.View; import android.view.ViewGroup.LayoutParams; @@ -61,15 +61,17 @@ public class SystemWebChromeClient extends WebChromeClient { // 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; - dialogsHelper = new CordovaDialogsHelper(parentEngine.webView.getContext()); + appContext = parentEngine.webView.getContext(); + dialogsHelper = new CordovaDialogsHelper(appContext); } /** @@ -174,14 +176,23 @@ public class SystemWebChromeClient extends WebChromeClient { /** * 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) { @@ -201,9 +212,9 @@ public class SystemWebChromeClient extends WebChromeClient { */ public View getVideoLoadingProgressView() { - if (mVideoProgressView == null) { + if (mVideoProgressView == null) { // Create a new Loading view programmatically. - + // create the linear layout LinearLayout layout = new LinearLayout(parentEngine.getView().getContext()); layout.setOrientation(LinearLayout.VERTICAL); @@ -214,12 +225,12 @@ public class SystemWebChromeClient extends WebChromeClient { 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); + bar.setLayoutParams(barLayoutParams); layout.addView(bar); - + mVideoProgressView = layout; } - return mVideoProgressView; + return mVideoProgressView; } // <input type=file> support: @@ -228,11 +239,11 @@ public class SystemWebChromeClient extends WebChromeClient { public void openFileChooser(ValueCallback<Uri> uploadMsg) { this.openFileChooser(uploadMsg, "*/*"); } - + public void openFileChooser( ValueCallback<Uri> uploadMsg, String acceptType ) { this.openFileChooser(uploadMsg, acceptType, null); } - + public void openFileChooser(final ValueCallback<Uri> uploadMsg, String acceptType, String capture) { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); @@ -242,7 +253,7 @@ public class SystemWebChromeClient extends WebChromeClient { @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); + LOG.d(LOG_TAG, "Receive file chooser URL: " + result); uploadMsg.onReceiveValue(result); } }, intent, FILECHOOSER_RESULTCODE); @@ -257,12 +268,12 @@ public class SystemWebChromeClient extends WebChromeClient { @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); + 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); + LOG.w("No activity found to handle file chooser intent.", e); filePathsCallback.onReceiveValue(null); } return true; @@ -271,7 +282,7 @@ public class SystemWebChromeClient extends WebChromeClient { @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Override public void onPermissionRequest(final PermissionRequest request) { - Log.d(LOG_TAG, "onPermissionRequest: " + Arrays.toString(request.getResources())); + LOG.d(LOG_TAG, "onPermissionRequest: " + Arrays.toString(request.getResources())); request.grant(request.getResources()); } diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemWebView.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemWebView.java index 01c2f000..01c2f000 100755..100644 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemWebView.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemWebView.java diff --git a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemWebViewEngine.java b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemWebViewEngine.java index 221a884c..0fa02767 100755 --- a/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemWebViewEngine.java +++ b/StoneIsland/platforms/android/CordovaLib/src/org/apache/cordova/engine/SystemWebViewEngine.java @@ -27,8 +27,8 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.os.Build; -import android.util.Log; import android.view.View; +import android.webkit.ValueCallback; import android.webkit.WebSettings; import android.webkit.WebSettings.LayoutAlgorithm; import android.webkit.WebView; @@ -40,6 +40,7 @@ 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; @@ -116,7 +117,9 @@ public class SystemWebViewEngine implements CordovaWebViewEngine { SystemWebViewEngine.this.cordova.getActivity().runOnUiThread(r); } })); - bridge = new CordovaBridge(pluginManager, nativeToJsMessageQueue); + if(Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) + nativeToJsMessageQueue.addBridgeMode(new NativeToJsMessageQueue.EvalBridgeMode(this, cordova)); + bridge = new CordovaBridge(pluginManager, nativeToJsMessageQueue); exposeJsInterface(webView, bridge); } @@ -135,7 +138,7 @@ public class SystemWebViewEngine implements CordovaWebViewEngine { return webView; } - @SuppressLint("SetJavaScriptEnabled") + @SuppressLint({"NewApi", "SetJavaScriptEnabled"}) @SuppressWarnings("deprecation") private void initWebViewSettings() { webView.setInitialScale(0); @@ -145,32 +148,32 @@ public class SystemWebViewEngine implements CordovaWebViewEngine { settings.setJavaScriptEnabled(true); settings.setJavaScriptCanOpenWindowsAutomatically(true); settings.setLayoutAlgorithm(LayoutAlgorithm.NORMAL); - + // Set the nav dump for HTC 2.x devices (disabling for ICS, deprecated entirely for Jellybean 4.2) try { Method gingerbread_getMethod = WebSettings.class.getMethod("setNavDump", new Class[] { boolean.class }); - + String manufacturer = android.os.Build.MANUFACTURER; - Log.d(TAG, "CordovaWebView is running on device made by: " + manufacturer); + LOG.d(TAG, "CordovaWebView is running on device made by: " + manufacturer); if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB && android.os.Build.MANUFACTURER.contains("HTC")) { gingerbread_getMethod.invoke(settings, true); } } catch (NoSuchMethodException e) { - Log.d(TAG, "We are on a modern version of Android, we will deprecate HTC 2.3 devices in 2.8"); + LOG.d(TAG, "We are on a modern version of Android, we will deprecate HTC 2.3 devices in 2.8"); } catch (IllegalArgumentException e) { - Log.d(TAG, "Doing the NavDump failed with bad arguments"); + LOG.d(TAG, "Doing the NavDump failed with bad arguments"); } catch (IllegalAccessException e) { - Log.d(TAG, "This should never happen: IllegalAccessException means this isn't Android anymore"); + LOG.d(TAG, "This should never happen: IllegalAccessException means this isn't Android anymore"); } catch (InvocationTargetException e) { - Log.d(TAG, "This should never happen: InvocationTargetException means this isn't Android anymore."); + LOG.d(TAG, "This should never happen: InvocationTargetException means this isn't Android anymore."); } //We don't save any form data in the application settings.setSaveFormData(false); settings.setSavePassword(false); - + // Jellybean rightfully tried to lock this down. Too bad they didn't give us a whitelist // while we do this if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { @@ -184,15 +187,15 @@ public class SystemWebViewEngine implements CordovaWebViewEngine { 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 && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { enableRemoteDebugging(); } - + settings.setGeolocationDatabasePath(databasePath); // Enable DOM storage @@ -200,13 +203,13 @@ public class SystemWebViewEngine implements CordovaWebViewEngine { // 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(); @@ -222,7 +225,7 @@ public class SystemWebViewEngine implements CordovaWebViewEngine { } } // End CB-3360 - + IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); if (this.receiver == null) { @@ -242,18 +245,18 @@ public class SystemWebViewEngine implements CordovaWebViewEngine { try { WebView.setWebContentsDebuggingEnabled(true); } catch (IllegalArgumentException e) { - Log.d(TAG, "You have one job! To turn on Remote Web Debugging! YOU HAVE FAILED! "); + LOG.d(TAG, "You have one job! To turn on Remote Web Debugging! YOU HAVE FAILED! "); e.printStackTrace(); } } private static void exposeJsInterface(WebView webView, CordovaBridge bridge) { if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1)) { - Log.i(TAG, "Disabled addJavascriptInterface() bridge since Android version is old."); + LOG.i(TAG, "Disabled addJavascriptInterface() bridge since Android version is old."); // Bug being that Java Strings do not get converted to JS strings automatically. // This isn't hard to work-around on the JS side, but it's easier to just // use the prompt bridge instead. - return; + return; } SystemExposedJsApi exposedJsApi = new SystemExposedJsApi(bridge); webView.addJavascriptInterface(exposedJsApi, "_cordovaNative"); @@ -312,8 +315,10 @@ public class SystemWebViewEngine implements CordovaWebViewEngine { @Override public void setPaused(boolean value) { if (value) { + webView.onPause(); webView.pauseTimers(); } else { + webView.onResume(); webView.resumeTimers(); } } @@ -327,8 +332,19 @@ public class SystemWebViewEngine implements CordovaWebViewEngine { try { webView.getContext().unregisterReceiver(receiver); } catch (Exception e) { - Log.e(TAG, "Error unregistering configuration receiver: " + e.getMessage(), e); + LOG.e(TAG, "Error unregistering configuration receiver: " + e.getMessage(), e); } } } + + @Override + public void evaluateJavascript(String js, ValueCallback<String> callback) { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + webView.evaluateJavascript(js, callback); + } + else + { + LOG.d(TAG, "This webview is using the old bridge"); + } + } } diff --git a/StoneIsland/platforms/android/android.json b/StoneIsland/platforms/android/android.json index 587c7239..d345c65d 100755 --- a/StoneIsland/platforms/android/android.json +++ b/StoneIsland/platforms/android/android.json @@ -51,6 +51,10 @@ { "xml": "<feature name=\"Keyboard\"><param name=\"android-package\" value=\"io.ionic.keyboard.IonicKeyboard\" /><param name=\"onload\" value=\"true\" /></feature>", "count": 1 + }, + { + "xml": "<feature name=\"PushNotification\"><param name=\"android-package\" value=\"com.adobe.phonegap.push.PushPlugin\" /></feature>", + "count": 1 } ] } @@ -73,20 +77,44 @@ { "xml": "<receiver android:name=\"com.parse.GcmBroadcastReceiver\" android:permission=\"com.google.android.c2dm.permission.SEND\"><intent-filter><action android:name=\"com.google.android.c2dm.intent.RECEIVE\" /><action android:name=\"com.google.android.c2dm.intent.REGISTRATION\" /><category android:name=\"us.okfoc.stoneisland\" /></intent-filter></receiver>", "count": 1 + }, + { + "xml": "<activity android:exported=\"true\" android:name=\"com.adobe.phonegap.push.PushHandlerActivity\" android:permission=\"${applicationId}.permission.PushHandlerActivity\" />", + "count": 1 + }, + { + "xml": "<receiver android:name=\"com.adobe.phonegap.push.BackgroundActionButtonHandler\" />", + "count": 1 + }, + { + "xml": "<receiver android:exported=\"true\" android:name=\"com.google.android.gms.gcm.GcmReceiver\" android:permission=\"com.google.android.c2dm.permission.SEND\"><intent-filter><action android:name=\"com.google.android.c2dm.intent.RECEIVE\" /><category android:name=\"${applicationId}\" /></intent-filter></receiver>", + "count": 1 + }, + { + "xml": "<service android:exported=\"false\" android:name=\"com.adobe.phonegap.push.GCMIntentService\"><intent-filter><action android:name=\"com.google.android.c2dm.intent.RECEIVE\" /></intent-filter></service>", + "count": 1 + }, + { + "xml": "<service android:exported=\"false\" android:name=\"com.adobe.phonegap.push.PushInstanceIDListenerService\"><intent-filter><action android:name=\"com.google.android.gms.iid.InstanceID\" /></intent-filter></service>", + "count": 1 + }, + { + "xml": "<service android:exported=\"false\" android:name=\"com.adobe.phonegap.push.RegistrationIntentService\" />", + "count": 1 } ], "/manifest": [ { "xml": "<uses-permission android:name=\"android.permission.INTERNET\" />", - "count": 1 + "count": 2 }, { "xml": "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />", - "count": 1 + "count": 2 }, { "xml": "<uses-permission android:name=\"android.permission.WAKE_LOCK\" />", - "count": 1 + "count": 2 }, { "xml": "<uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\" />", @@ -94,7 +122,7 @@ }, { "xml": "<uses-permission android:name=\"android.permission.VIBRATE\" />", - "count": 1 + "count": 2 }, { "xml": "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\" />", @@ -102,7 +130,7 @@ }, { "xml": "<uses-permission android:name=\"com.google.android.c2dm.permission.RECEIVE\" />", - "count": 1 + "count": 2 }, { "xml": "<permission android:name=\"us.okfoc.stoneisland.permission.C2D_MESSAGE\" android:protectionLevel=\"signature\" />", @@ -111,6 +139,22 @@ { "xml": "<uses-permission android:name=\"us.okfoc.stoneisland.permission.C2D_MESSAGE\" />", "count": 1 + }, + { + "xml": "<uses-permission android:name=\"${applicationId}.permission.C2D_MESSAGE\" />", + "count": 1 + }, + { + "xml": "<uses-permission android:name=\"${applicationId}.permission.PushHandlerActivity\" />", + "count": 1 + }, + { + "xml": "<permission android:name=\"${applicationId}.permission.C2D_MESSAGE\" android:protectionLevel=\"signature\" />", + "count": 1 + }, + { + "xml": "<permission android:name=\"${applicationId}.permission.PushHandlerActivity\" android:protectionLevel=\"signature\" />", + "count": 1 } ], "/*/application/activity": [], @@ -161,6 +205,16 @@ } ] } + }, + "res/values/strings.xml": { + "parents": { + "/resources": [ + { + "xml": "<string name=\"google_app_id\">XXXXXXX</string>", + "count": 1 + } + ] + } } } }, @@ -209,6 +263,10 @@ }, "ionic-plugin-keyboard": { "PACKAGE_NAME": "us.okfoc.stoneisland" + }, + "phonegap-plugin-push": { + "SENDER_ID": "XXXXXXX", + "PACKAGE_NAME": "us.okfoc.stoneisland" } }, "dependent_plugins": {}, @@ -317,6 +375,14 @@ "cordova.plugins.Keyboard" ], "runs": true + }, + { + "id": "phonegap-plugin-push.PushNotification", + "file": "plugins/phonegap-plugin-push/www/push.js", + "pluginId": "phonegap-plugin-push", + "clobbers": [ + "PushNotification" + ] } ], "plugin_metadata": { @@ -332,6 +398,7 @@ "cordova-plugin-splashscreen": "4.0.0", "cordova-plugin-compat": "1.1.0", "cordova-plugin-geolocation": "2.4.0", - "ionic-plugin-keyboard": "2.2.1" + "ionic-plugin-keyboard": "2.2.1", + "phonegap-plugin-push": "1.9.2" } }
\ No newline at end of file diff --git a/StoneIsland/platforms/android/assets/www/cordova-js-src/android/nativeapiprovider.js b/StoneIsland/platforms/android/assets/www/cordova-js-src/android/nativeapiprovider.js index 2e9aa67b..2e9aa67b 100755..100644 --- a/StoneIsland/platforms/android/assets/www/cordova-js-src/android/nativeapiprovider.js +++ b/StoneIsland/platforms/android/assets/www/cordova-js-src/android/nativeapiprovider.js diff --git a/StoneIsland/platforms/android/assets/www/cordova-js-src/android/promptbasednativeapi.js b/StoneIsland/platforms/android/assets/www/cordova-js-src/android/promptbasednativeapi.js index f7fb6bc7..f7fb6bc7 100755..100644 --- a/StoneIsland/platforms/android/assets/www/cordova-js-src/android/promptbasednativeapi.js +++ b/StoneIsland/platforms/android/assets/www/cordova-js-src/android/promptbasednativeapi.js diff --git a/StoneIsland/platforms/android/assets/www/cordova-js-src/exec.js b/StoneIsland/platforms/android/assets/www/cordova-js-src/exec.js index fa8b41be..f73d87a1 100755..100644 --- a/StoneIsland/platforms/android/assets/www/cordova-js-src/exec.js +++ b/StoneIsland/platforms/android/assets/www/cordova-js-src/exec.js @@ -51,10 +51,11 @@ var cordova = require('cordova'), // 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 + ONLINE_EVENT: 2, + EVAL_BRIDGE: 3 }, jsToNativeBridgeMode, // Set lazily. - nativeToJsBridgeMode = nativeToJsModes.ONLINE_EVENT, + nativeToJsBridgeMode = nativeToJsModes.EVAL_BRIDGE, pollEnabled = false, bridgeSecret = -1; @@ -77,6 +78,9 @@ function androidExec(success, fail, service, action, args) { 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') { @@ -86,7 +90,6 @@ function androidExec(success, fail, service, action, args) { var callbackId = service + cordova.callbackId++, argsJson = JSON.stringify(args); - if (success || fail) { cordova.callbacks[callbackId] = {success:success, fail:fail}; } @@ -106,6 +109,17 @@ function androidExec(success, fail, service, action, args) { } 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(); }; diff --git a/StoneIsland/platforms/android/assets/www/cordova-js-src/platform.js b/StoneIsland/platforms/android/assets/www/cordova-js-src/platform.js index bffc6751..2bfd0247 100755..100644 --- a/StoneIsland/platforms/android/assets/www/cordova-js-src/platform.js +++ b/StoneIsland/platforms/android/assets/www/cordova-js-src/platform.js @@ -19,6 +19,9 @@ * */ +// The last resume event that was received that had the result of a plugin call. +var lastResumeEvent = null; + module.exports = { id: 'android', bootstrap: function() { @@ -58,6 +61,19 @@ module.exports = { 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() { @@ -79,12 +95,30 @@ function onMessageFromNative(msg) { case 'searchbutton': // App life cycle events case 'pause': - case 'resume': // 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/StoneIsland/platforms/android/assets/www/cordova-js-src/plugin/android/app.js b/StoneIsland/platforms/android/assets/www/cordova-js-src/plugin/android/app.js index 22cf96e8..22cf96e8 100755..100644 --- a/StoneIsland/platforms/android/assets/www/cordova-js-src/plugin/android/app.js +++ b/StoneIsland/platforms/android/assets/www/cordova-js-src/plugin/android/app.js diff --git a/StoneIsland/platforms/android/assets/www/cordova.js b/StoneIsland/platforms/android/assets/www/cordova.js index 23f6e475..18c020e7 100755..100644 --- a/StoneIsland/platforms/android/assets/www/cordova.js +++ b/StoneIsland/platforms/android/assets/www/cordova.js @@ -1,5 +1,5 @@ // Platform: android -// 2c29e187e4206a6a77fba940ef6f77aef5c7eb8c +// 7c5fcc5a5adfbf3fb8ceaf36fbdd4bd970bd9c20 /* Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file @@ -19,7 +19,7 @@ under the License. */ ;(function() { -var PLATFORM_VERSION_BUILD_LABEL = '4.1.1'; +var PLATFORM_VERSION_BUILD_LABEL = '6.1.2'; // file: src/scripts/require.js /*jshint -W079 */ @@ -101,7 +101,9 @@ if (typeof module === "object" && typeof require === "function") { // file: src/cordova.js define("cordova", function(require, exports, module) { -if(window.cordova){ +// 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)) { throw new Error("cordova already defined"); } @@ -740,8 +742,13 @@ var Channel = function(type, sticky) { } }; -function forceFunction(f) { - if (typeof f != 'function') throw "Function required as first argument!"; +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." + ); + } } /** @@ -751,28 +758,39 @@ function forceFunction(f) { * and a guid that can be used to stop subscribing to the channel. * Returns the guid. */ -Channel.prototype.subscribe = function(f, c) { - // need a function to call - forceFunction(f); +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) { - f.apply(c || this, this.fireArgs); + handleEvent.apply(eventListener || this, this.fireArgs); return; } - var func = f, - guid = f.observer_guid; - if (typeof c == "object") { func = utils.close(c, f); } + guid = eventListenerOrFunction.observer_guid; + if (typeof eventListener === "object") { + handleEvent = utils.close(eventListener, handleEvent); + } if (!guid) { - // first time any channel has seen this subscriber + // First time any channel has seen this subscriber guid = '' + nextGuid++; } - func.observer_guid = guid; - f.observer_guid = guid; + handleEvent.observer_guid = guid; + eventListenerOrFunction.observer_guid = guid; // Don't add the same handler more than once. if (!this.handlers[guid]) { - this.handlers[guid] = func; + this.handlers[guid] = handleEvent; this.numHandlers++; if (this.numHandlers == 1) { this.onHasSubscribersChange && this.onHasSubscribersChange(); @@ -783,12 +801,20 @@ Channel.prototype.subscribe = function(f, c) { /** * Unsubscribes the function with the given guid from the channel. */ -Channel.prototype.unsubscribe = function(f) { - // need a function to unsubscribe - forceFunction(f); +Channel.prototype.unsubscribe = function(eventListenerOrFunction) { + checkSubscriptionArgument(eventListenerOrFunction); + var handleEvent, guid, handler; - var guid = f.observer_guid, - handler = this.handlers[guid]; + 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--; @@ -895,10 +921,11 @@ var cordova = require('cordova'), // 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 + ONLINE_EVENT: 2, + EVAL_BRIDGE: 3 }, jsToNativeBridgeMode, // Set lazily. - nativeToJsBridgeMode = nativeToJsModes.ONLINE_EVENT, + nativeToJsBridgeMode = nativeToJsModes.EVAL_BRIDGE, pollEnabled = false, bridgeSecret = -1; @@ -921,6 +948,9 @@ function androidExec(success, fail, service, action, args) { 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') { @@ -930,7 +960,6 @@ function androidExec(success, fail, service, action, args) { var callbackId = service + cordova.callbackId++, argsJson = JSON.stringify(args); - if (success || fail) { cordova.callbacks[callbackId] = {success:success, fail:fail}; } @@ -950,6 +979,17 @@ function androidExec(success, fail, service, action, args) { } 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(); }; @@ -1291,10 +1331,12 @@ 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]; +var platformInitChannelsArray = [channel.onDOMContentLoaded, channel.onNativeReady, channel.onPluginsReady]; // setting exec cordova.exec = require('cordova/exec'); @@ -1379,10 +1421,19 @@ if (window._nativeReady) { // 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(); @@ -1501,9 +1552,109 @@ exports.reset(); }); +// file: src/common/modulemapper_b.js +define("cordova/modulemapper_b", function(require, exports, module) { + +var builder = require('cordova/builder'), + symbolList = [], + 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) { + 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); + // <runs/> + 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() { @@ -1543,6 +1694,19 @@ module.exports = { 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() { @@ -1564,12 +1728,30 @@ function onMessageFromNative(msg) { case 'searchbutton': // App life cycle events case 'pause': - case 'resume': // 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); } @@ -1673,10 +1855,6 @@ module.exports = { // file: src/common/pluginloader.js define("cordova/pluginloader", function(require, exports, module) { -/* - NOTE: this file is NOT used when we use the browserify workflow -*/ - var modulemapper = require('cordova/modulemapper'); var urlutil = require('cordova/urlutil'); @@ -1786,6 +1964,54 @@ exports.load = function(callback) { }); +// file: src/common/pluginloader_b.js +define("cordova/pluginloader_b", function(require, exports, module) { + +var modulemapper = require('cordova/modulemapper'); + +// Handler for the cordova_plugins.js content. +// See plugman's plugin_loader.js for the details of this object. +function handlePluginsObject(moduleList) { + // if moduleList is not defined or empty, we've nothing to do + if (!moduleList || !moduleList.length) { + return; + } + + // Loop through all the modules and then through their clobbers and merges. + for (var i = 0, module; module = moduleList[i]; i++) { + if (module.clobbers && module.clobbers.length) { + for (var j = 0; j < module.clobbers.length; j++) { + modulemapper.clobbers(module.id, module.clobbers[j]); + } + } + + if (module.merges && module.merges.length) { + for (var k = 0; k < module.merges.length; k++) { + modulemapper.merges(module.id, module.merges[k]); + } + } + + // Finally, if runs is truthy we want to simply require() the module. + if (module.runs) { + modulemapper.runs(module.id); + } + } +} + +// Loads all plugins' js-modules. Plugin loading is syncronous in browserified bundle +// but the method accepts callback to be compatible with non-browserify flow. +// onDeviceReady is blocked on onPluginsReady. onPluginsReady is fired when there are +// no plugins to load, or they are all done. +exports.load = function(callback) { + var moduleList = require("cordova/plugin_list"); + handlePluginsObject(moduleList); + + callback(); +}; + + +}); + // file: src/common/urlutil.js define("cordova/urlutil", function(require, exports, module) { @@ -1895,7 +2121,10 @@ utils.clone = function(obj) { retVal = {}; for(i in obj){ - if(!(i in retVal) || retVal[i] != obj[i]) { + // https://issues.apache.org/jira/browse/CB-11522 'unknown' type may be returned in + // custom protocol activation case on Windows Phone 8.1 causing "No such interface supported" exception + // on cloning. + if((!(i in retVal) || retVal[i] != obj[i]) && typeof obj[i] != 'undefined' && typeof obj[i] != 'unknown') { retVal[i] = utils.clone(obj[i]); } } diff --git a/StoneIsland/platforms/android/assets/www/cordova_plugins.js b/StoneIsland/platforms/android/assets/www/cordova_plugins.js index a2734881..722b1683 100755 --- a/StoneIsland/platforms/android/assets/www/cordova_plugins.js +++ b/StoneIsland/platforms/android/assets/www/cordova_plugins.js @@ -104,6 +104,14 @@ module.exports = [ "cordova.plugins.Keyboard" ], "runs": true + }, + { + "id": "phonegap-plugin-push.PushNotification", + "file": "plugins/phonegap-plugin-push/www/push.js", + "pluginId": "phonegap-plugin-push", + "clobbers": [ + "PushNotification" + ] } ]; module.exports.metadata = @@ -121,7 +129,8 @@ module.exports.metadata = "cordova-plugin-splashscreen": "4.0.0", "cordova-plugin-compat": "1.1.0", "cordova-plugin-geolocation": "2.4.0", - "ionic-plugin-keyboard": "2.2.1" -} + "ionic-plugin-keyboard": "2.2.1", + "phonegap-plugin-push": "1.9.2" +}; // BOTTOM OF METADATA });
\ No newline at end of file diff --git a/StoneIsland/platforms/android/assets/www/css/products.css b/StoneIsland/platforms/android/assets/www/css/products.css index e067e05a..bdfed42a 100755 --- a/StoneIsland/platforms/android/assets/www/css/products.css +++ b/StoneIsland/platforms/android/assets/www/css/products.css @@ -24,7 +24,6 @@ height: 126vw; } - .product #product { display: block } #product { display: none; @@ -224,7 +223,11 @@ padding-bottom:45px; } #collection h1 { -background:white + background-color: white; + background-image: url(../img/angle-down.png); + background-size: contain; + background-position: top right; + background-repeat: no-repeat; } #selector { diff --git a/StoneIsland/platforms/android/assets/www/img/angle-down.png b/StoneIsland/platforms/android/assets/www/img/angle-down.png Binary files differnew file mode 100644 index 00000000..e0b3b00f --- /dev/null +++ b/StoneIsland/platforms/android/assets/www/img/angle-down.png diff --git a/StoneIsland/platforms/android/assets/www/index.html b/StoneIsland/platforms/android/assets/www/index.html index 768e3140..9c86e559 100755 --- a/StoneIsland/platforms/android/assets/www/index.html +++ b/StoneIsland/platforms/android/assets/www/index.html @@ -355,7 +355,7 @@ <input type="email" name="Email" placeholder="EMAIL ADDRESS" required> <input type="email" name="ConfirmEmail" placeholder="CONFIRM EMAIL ADDRESS" required> <div class="select-wrapper date-wrapper"> - <span>BIRTHDAY (MM/DD/YYYY)</span> + <span>BIRTHDAY (OPTIONAL)</span> <input type="date" name="BirthDay" min="1900-01-01" placeholder="BIRTHDAY (MM/DD/YYYY)" required> </div> @@ -1136,6 +1136,7 @@ <script src="js/lib/products/CollectionView.js"></script> <script src="js/lib/products/filters/CategoryFilter.js"></script> +<script src="js/lib/products/filters/DepartmentFilter.js"></script> <script src="js/lib/products/ClosedStoreView.js"></script> <script src="js/lib/products/ProductView.js"></script> <script src="js/lib/products/GalleryView.js"></script> diff --git a/StoneIsland/platforms/android/assets/www/js/index.js b/StoneIsland/platforms/android/assets/www/js/index.js index 6bea75d0..76a8af05 100755 --- a/StoneIsland/platforms/android/assets/www/js/index.js +++ b/StoneIsland/platforms/android/assets/www/js/index.js @@ -8,7 +8,7 @@ var app = (function(){ app.bind() app.build() - + app.iscroll_options = { mouseWheel: true, scrollbars: true, diff --git a/StoneIsland/platforms/android/assets/www/js/lib/auth/SignupView.js b/StoneIsland/platforms/android/assets/www/js/lib/auth/SignupView.js index 8d9cf52d..fd0c1887 100755 --- a/StoneIsland/platforms/android/assets/www/js/lib/auth/SignupView.js +++ b/StoneIsland/platforms/android/assets/www/js/lib/auth/SignupView.js @@ -49,7 +49,7 @@ var SignupView = FormView.extend({ "Surname": "Please enter your last name.", "Email": "Please enter a valid email address.", "ConfirmEmail": "Please enter a valid email address.", - "BirthDay": "Please enter your birthday.", + // "BirthDay": "Please enter your birthday.", "Password": "Please enter your password.", "Password2": "Please enter your password again.", "DataProfiling": "You must agree to data profiling.", diff --git a/StoneIsland/platforms/android/assets/www/js/lib/blogs/BlogView.js b/StoneIsland/platforms/android/assets/www/js/lib/blogs/BlogView.js index b7c80520..5ee7f641 100755 --- a/StoneIsland/platforms/android/assets/www/js/lib/blogs/BlogView.js +++ b/StoneIsland/platforms/android/assets/www/js/lib/blogs/BlogView.js @@ -31,7 +31,7 @@ var BlogView = View.extend({ this.loaded = true this.data = data = typeof data == "string" ? JSON.parse(data) : data - switch (data.store[0].StoreStatus) { + switch (data.store[0].DepartmentStoreStatus) { case "open": app.closed.storeIsClosed = false break @@ -46,9 +46,9 @@ var BlogView = View.extend({ app.closed.populate(data.store[0].ClosedStoreImages) } else { - app.gallery_id = data.store[0].CollectionId - app.department_id = data.store[0].DepartmentId - app.collection.setCollectionName( data.store[0].collection ) + app.departments = data.store[0].Departments + app.department_id = data.store[0].Departments[0].uri + app.collection.setCollectionName( data.store[0].Departments[0].text ) app.collection.loaded = false app.collection.fetch() } diff --git a/StoneIsland/platforms/android/assets/www/js/lib/blogs/HubView.js b/StoneIsland/platforms/android/assets/www/js/lib/blogs/HubView.js index 1657d295..013c2b45 100755 --- a/StoneIsland/platforms/android/assets/www/js/lib/blogs/HubView.js +++ b/StoneIsland/platforms/android/assets/www/js/lib/blogs/HubView.js @@ -167,6 +167,7 @@ var HubLoader = (function(){ } HubLoader.build = function(){ view.append(item) + view.scroller.refresh() setTimeout(HubLoader.load, 20) } return HubLoader diff --git a/StoneIsland/platforms/android/assets/www/js/lib/products/CollectionView.js b/StoneIsland/platforms/android/assets/www/js/lib/products/CollectionView.js index b9a7755f..671d36b3 100755 --- a/StoneIsland/platforms/android/assets/www/js/lib/products/CollectionView.js +++ b/StoneIsland/platforms/android/assets/www/js/lib/products/CollectionView.js @@ -14,6 +14,7 @@ var CollectionView = ScrollableView.extend({ "mousedown .item": "touchstart", "mousemove .item": "touchmove", "mouseup .item": "touchend", + "touchstart h1": "showDepartmentSelector", }, initialize: function(){ @@ -21,7 +22,7 @@ var CollectionView = ScrollableView.extend({ this.$content = this.$(".content") this.$loader = this.$(".loader") this.scroller = new IScroll('#collection', app.iscroll_options) - this.filterView = new CategoryFilter ({ parent: this }) + this.filterView = new DepartmentFilter ({ parent: this }) }, show: function(){ @@ -85,7 +86,7 @@ var CollectionView = ScrollableView.extend({ } console.log(">>>>>>>> YES ") if (! this.loaded) { - console.log("populate 3") + console.log("populate 3", data.SearchResponseFull.Results.Items.length) this.loaded = true this.$loader.hide() this.$content.empty() @@ -140,6 +141,10 @@ var CollectionView = ScrollableView.extend({ this.$title.html(this.collectionName) }, + showDepartmentSelector: function(){ + this.filterView.filter() + }, + firstTouch: { x: 0, y: 0, id: "" }, lastTouch: { x: 0, y: 0, id: "" }, touchstart: function(e){ diff --git a/StoneIsland/platforms/android/assets/www/js/lib/products/ProductView.js b/StoneIsland/platforms/android/assets/www/js/lib/products/ProductView.js index cfb39f20..81ad536d 100755 --- a/StoneIsland/platforms/android/assets/www/js/lib/products/ProductView.js +++ b/StoneIsland/platforms/android/assets/www/js/lib/products/ProductView.js @@ -118,8 +118,8 @@ var ProductView = ScrollableView.extend({ var title = name_partz.join(' ') var type = title_case( data['MicroCategory'] ) var price = "$" + data['DiscountedPrice'] + ".00" - var body = descriptions['Details'] + "<br>" + descriptions['EditorialDescription'] - body = body.replace(/<br>/g, "<br><br>").replace(/(<br>)+$/, "") + var body = descriptions['Details'] + " " + descriptions['EditorialDescription'] + // body = body.replace(/<br>/g, "<br><br>").replace(/(<br>)+$/, "") var default_color_id = this.populate_selectors(data, details) diff --git a/StoneIsland/platforms/android/assets/www/js/lib/products/Selector.js b/StoneIsland/platforms/android/assets/www/js/lib/products/Selector.js index 6db33b68..9c1109f6 100755 --- a/StoneIsland/platforms/android/assets/www/js/lib/products/Selector.js +++ b/StoneIsland/platforms/android/assets/www/js/lib/products/Selector.js @@ -5,7 +5,7 @@ var Selector = View.extend({ events: { "click .close": "hide", - "click .options div": "pick", + "touchstart .options div": "pick", }, initialize: function(){ @@ -67,4 +67,4 @@ var Selector = View.extend({ this.hide() }, -})
\ No newline at end of file +}) diff --git a/StoneIsland/platforms/android/assets/www/js/lib/products/filters/DepartmentFilter.js b/StoneIsland/platforms/android/assets/www/js/lib/products/filters/DepartmentFilter.js new file mode 100644 index 00000000..cc0d925e --- /dev/null +++ b/StoneIsland/platforms/android/assets/www/js/lib/products/filters/DepartmentFilter.js @@ -0,0 +1,31 @@ +var DepartmentFilter = View.extend({ + + initialize: function(opt){ + this.parent = opt.parent + }, + + filter: function(){ + var deps = app.departments.map(function(dep){ + console.log(dep) + return { + id: dep.uri, + label: dep.text, + } + }) + app.selector.select("wide", deps, this.pick.bind(this)) + }, + + last_choice: null, + + pick: function(choice){ + this.parent.$content.empty() + this.last_choice = choice + app.department_id = choice.id + app.collection.loaded = false + app.collection.fetch() + app.footer.hide() + app.collection.setCollectionName(choice.label) + this.parent.deferScrollToTop() + }, + +}) diff --git a/StoneIsland/platforms/android/assets/www/plugins/phonegap-plugin-push/www/push.js b/StoneIsland/platforms/android/assets/www/plugins/phonegap-plugin-push/www/push.js new file mode 100644 index 00000000..a5315486 --- /dev/null +++ b/StoneIsland/platforms/android/assets/www/plugins/phonegap-plugin-push/www/push.js @@ -0,0 +1,329 @@ +cordova.define("phonegap-plugin-push.PushNotification", function(require, exports, module) { +/* global cordova:false */ +/* globals window */ + +/*! + * Module dependencies. + */ + +var exec = cordova.require('cordova/exec'); + +/** + * PushNotification constructor. + * + * @param {Object} options to initiate Push Notifications. + * @return {PushNotification} instance that can be monitored and cancelled. + */ + +var PushNotification = function(options) { + this._handlers = { + 'registration': [], + 'notification': [], + 'error': [] + }; + + // require options parameter + if (typeof options === 'undefined') { + throw new Error('The options argument is required.'); + } + + // store the options to this object instance + this.options = options; + + // triggered on registration and notification + var that = this; + var success = function(result) { + if (result && typeof result.registrationId !== 'undefined') { + that.emit('registration', result); + } else if (result && result.additionalData && typeof result.additionalData.actionCallback !== 'undefined') { + var executeFuctionOrEmitEventByName = function(callbackName, context, arg) { + var namespaces = callbackName.split('.'); + var func = namespaces.pop(); + for (var i = 0; i < namespaces.length; i++) { + context = context[namespaces[i]]; + } + + if (typeof context[func] === 'function') { + context[func].call(context, arg); + } else { + that.emit(callbackName, arg); + } + }; + + executeFuctionOrEmitEventByName(result.additionalData.actionCallback, window, result); + } else if (result) { + that.emit('notification', result); + } + }; + + // triggered on error + var fail = function(msg) { + var e = (typeof msg === 'string') ? new Error(msg) : msg; + that.emit('error', e); + }; + + // wait at least one process tick to allow event subscriptions + setTimeout(function() { + exec(success, fail, 'PushNotification', 'init', [options]); + }, 10); +}; + +/** + * Unregister from push notifications + */ + +PushNotification.prototype.unregister = function(successCallback, errorCallback, options) { + if (!errorCallback) { errorCallback = function() {}; } + + if (typeof errorCallback !== 'function') { + console.log('PushNotification.unregister failure: failure parameter not a function'); + return; + } + + if (typeof successCallback !== 'function') { + console.log('PushNotification.unregister failure: success callback parameter must be a function'); + return; + } + + var that = this; + var cleanHandlersAndPassThrough = function() { + if (!options) { + that._handlers = { + 'registration': [], + 'notification': [], + 'error': [] + }; + } + successCallback(); + }; + + exec(cleanHandlersAndPassThrough, errorCallback, 'PushNotification', 'unregister', [options]); +}; + +/** + * subscribe to a topic + * @param {String} topic topic to subscribe + * @param {Function} successCallback success callback + * @param {Function} errorCallback error callback + * @return {void} + */ +PushNotification.prototype.subscribe = function(topic, successCallback, errorCallback) { + if (!errorCallback) { errorCallback = function() {}; } + + if (typeof errorCallback !== 'function') { + console.log('PushNotification.subscribe failure: failure parameter not a function'); + return; + } + + if (typeof successCallback !== 'function') { + console.log('PushNotification.subscribe failure: success callback parameter must be a function'); + return; + } + + exec(successCallback, errorCallback, 'PushNotification', 'subscribe', [topic]); +}; + +/** + * unsubscribe to a topic + * @param {String} topic topic to unsubscribe + * @param {Function} successCallback success callback + * @param {Function} errorCallback error callback + * @return {void} + */ +PushNotification.prototype.unsubscribe = function(topic, successCallback, errorCallback) { + if (!errorCallback) { errorCallback = function() {}; } + + if (typeof errorCallback !== 'function') { + console.log('PushNotification.unsubscribe failure: failure parameter not a function'); + return; + } + + if (typeof successCallback !== 'function') { + console.log('PushNotification.unsubscribe failure: success callback parameter must be a function'); + return; + } + + exec(successCallback, errorCallback, 'PushNotification', 'unsubscribe', [topic]); +}; + +/** + * Call this to set the application icon badge + */ + +PushNotification.prototype.setApplicationIconBadgeNumber = function(successCallback, errorCallback, badge) { + if (!errorCallback) { errorCallback = function() {}; } + + if (typeof errorCallback !== 'function') { + console.log('PushNotification.setApplicationIconBadgeNumber failure: failure parameter not a function'); + return; + } + + if (typeof successCallback !== 'function') { + console.log('PushNotification.setApplicationIconBadgeNumber failure: success callback parameter must be a function'); + return; + } + + exec(successCallback, errorCallback, 'PushNotification', 'setApplicationIconBadgeNumber', [{badge: badge}]); +}; + +/** + * Get the application icon badge + */ + +PushNotification.prototype.getApplicationIconBadgeNumber = function(successCallback, errorCallback) { + if (!errorCallback) { errorCallback = function() {}; } + + if (typeof errorCallback !== 'function') { + console.log('PushNotification.getApplicationIconBadgeNumber failure: failure parameter not a function'); + return; + } + + if (typeof successCallback !== 'function') { + console.log('PushNotification.getApplicationIconBadgeNumber failure: success callback parameter must be a function'); + return; + } + + exec(successCallback, errorCallback, 'PushNotification', 'getApplicationIconBadgeNumber', []); +}; + +/** + * Get the application icon badge + */ + +PushNotification.prototype.clearAllNotifications = function(successCallback, errorCallback) { + if (!successCallback) { successCallback = function() {}; } + if (!errorCallback) { errorCallback = function() {}; } + + if (typeof errorCallback !== 'function') { + console.log('PushNotification.clearAllNotifications failure: failure parameter not a function'); + return; + } + + if (typeof successCallback !== 'function') { + console.log('PushNotification.clearAllNotifications failure: success callback parameter must be a function'); + return; + } + + exec(successCallback, errorCallback, 'PushNotification', 'clearAllNotifications', []); +}; + +/** + * Listen for an event. + * + * Any event is supported, but the following are built-in: + * + * - registration + * - notification + * - error + * + * @param {String} eventName to subscribe to. + * @param {Function} callback triggered on the event. + */ + +PushNotification.prototype.on = function(eventName, callback) { + if (!this._handlers.hasOwnProperty(eventName)) { + this._handlers[eventName] = []; + } + this._handlers[eventName].push(callback); +}; + +/** + * Remove event listener. + * + * @param {String} eventName to match subscription. + * @param {Function} handle function associated with event. + */ + +PushNotification.prototype.off = function (eventName, handle) { + if (this._handlers.hasOwnProperty(eventName)) { + var handleIndex = this._handlers[eventName].indexOf(handle); + if (handleIndex >= 0) { + this._handlers[eventName].splice(handleIndex, 1); + } + } +}; + +/** + * Emit an event. + * + * This is intended for internal use only. + * + * @param {String} eventName is the event to trigger. + * @param {*} all arguments are passed to the event listeners. + * + * @return {Boolean} is true when the event is triggered otherwise false. + */ + +PushNotification.prototype.emit = function() { + var args = Array.prototype.slice.call(arguments); + var eventName = args.shift(); + + if (!this._handlers.hasOwnProperty(eventName)) { + return false; + } + + for (var i = 0, length = this._handlers[eventName].length; i < length; i++) { + var callback = this._handlers[eventName][i]; + if (typeof callback === 'function') { + callback.apply(undefined,args); + } else { + console.log('event handler: ' + eventName + ' must be a function'); + } + } + + return true; +}; + +PushNotification.prototype.finish = function(successCallback, errorCallback, id) { + if (!successCallback) { successCallback = function() {}; } + if (!errorCallback) { errorCallback = function() {}; } + if (!id) { id = 'handler'; } + + if (typeof successCallback !== 'function') { + console.log('finish failure: success callback parameter must be a function'); + return; + } + + if (typeof errorCallback !== 'function') { + console.log('finish failure: failure parameter not a function'); + return; + } + + exec(successCallback, errorCallback, 'PushNotification', 'finish', [id]); +}; + +/*! + * Push Notification Plugin. + */ + +module.exports = { + /** + * Register for Push Notifications. + * + * This method will instantiate a new copy of the PushNotification object + * and start the registration process. + * + * @param {Object} options + * @return {PushNotification} instance + */ + + init: function(options) { + return new PushNotification(options); + }, + + hasPermission: function(successCallback, errorCallback) { + exec(successCallback, errorCallback, 'PushNotification', 'hasPermission', []); + }, + + /** + * PushNotification Object. + * + * Expose the PushNotification object for direct use + * and testing. Typically, you should use the + * .init helper method. + */ + + PushNotification: PushNotification +}; + +}); diff --git a/StoneIsland/platforms/android/build.gradle b/StoneIsland/platforms/android/build.gradle index b9c43320..5eef5255 100755..100644 --- a/StoneIsland/platforms/android/build.gradle +++ b/StoneIsland/platforms/android/build.gradle @@ -17,41 +17,35 @@ under the License. */ -// GENERATED FILE! DO NOT EDIT! - -apply plugin: 'android' +apply plugin: 'com.android.application' buildscript { repositories { mavenCentral() + jcenter() } // Switch the Android Gradle plugin version requirement depending on the // installed version of Gradle. This dependency is documented at // http://tools.android.com/tech-docs/new-build-system/version-compatibility // and https://issues.apache.org/jira/browse/CB-8143 - if (gradle.gradleVersion >= "2.2") { - dependencies { - classpath 'com.android.tools.build:gradle:1.0.0+' - } - } else if (gradle.gradleVersion >= "2.1") { - dependencies { - classpath 'com.android.tools.build:gradle:0.14.0+' - } - } else { - dependencies { - classpath 'com.android.tools.build:gradle:0.12.0+' - } + dependencies { + classpath "com.google.gms:google-services:3.0.0" + classpath "com.google.gms:google-services:3.0.0" + classpath 'com.android.tools.build:gradle:2.2.1' } } // Allow plugins to declare Maven dependencies via build-extras.gradle. -repositories { - mavenCentral() +allprojects { + repositories { + mavenCentral(); + jcenter() + } } task wrapper(type: Wrapper) { - gradleVersion = '2.2.1' + gradleVersion = '2.14.1' } // Configuration properties. Set these via environment variables, build-extras.gradle, or gradle.properties. @@ -173,25 +167,31 @@ android { } defaultConfig { - versionCode cdvVersionCode ?: Integer.parseInt("" + privateHelpers.extractIntFromManifest("versionCode") + "0") + versionCode cdvVersionCode ?: new BigInteger("" + privateHelpers.extractIntFromManifest("versionCode")) + applicationId privateHelpers.extractStringFromManifest("package") + if (cdvMinSdkVersion != null) { minSdkVersion cdvMinSdkVersion } } + lintOptions { + abortOnError false; + } + compileSdkVersion cdvCompileSdkVersion buildToolsVersion cdvBuildToolsVersion if (Boolean.valueOf(cdvBuildMultipleApks)) { productFlavors { armv7 { - versionCode cdvVersionCode ?: defaultConfig.versionCode + 2 + versionCode defaultConfig.versionCode*10 + 2 ndk { abiFilters "armeabi-v7a", "" } } x86 { - versionCode cdvVersionCode ?: defaultConfig.versionCode + 4 + versionCode defaultConfig.versionCode*10 + 4 ndk { abiFilters "x86", "" } @@ -202,7 +202,12 @@ android { } } } - } else if (!cdvVersionCode) { + } + /* + + ELSE NOTHING! DON'T MESS WITH THE VERSION CODE IF YOU DON'T HAVE TO! + + else if (!cdvVersionCode) { def minSdkVersion = cdvMinSdkVersion ?: privateHelpers.extractIntFromManifest("minSdkVersion") // Vary versionCode by the two most common API levels: // 14 is ICS, which is the lowest API level for many apps. @@ -213,6 +218,7 @@ android { defaultConfig.versionCode += 8 } } + */ compileOptions { sourceCompatibility JavaVersion.VERSION_1_6 @@ -244,10 +250,11 @@ android { dependencies { compile fileTree(dir: 'libs', include: '*.jar') // SUB-PROJECT DEPENDENCIES START - debugCompile project(path: "CordovaLib", configuration: "debug") - releaseCompile project(path: "CordovaLib", configuration: "release") + debugCompile(project(path: "CordovaLib", configuration: "debug")) + releaseCompile(project(path: "CordovaLib", configuration: "release")) compile "com.android.support:support-v13:23+" - compile "com.google.android.gms:play-services-gcm:+" + compile "com.google.android.gms:play-services-gcm:9.8+" + compile "me.leolin:ShortcutBadger:1.1.11@aar" // SUB-PROJECT DEPENDENCIES END } @@ -265,7 +272,7 @@ def promptForReleaseKeyPassword() { gradle.taskGraph.whenReady { taskGraph -> taskGraph.getAllTasks().each() { task -> - if (task.name == 'validateReleaseSigning') { + if (task.name == 'validateReleaseSigning' || task.name == 'validateSigningRelease') { promptForReleaseKeyPassword() } } diff --git a/StoneIsland/platforms/android/cordova/.jshintrc b/StoneIsland/platforms/android/cordova/.jshintrc new file mode 100644 index 00000000..89a121cf --- /dev/null +++ b/StoneIsland/platforms/android/cordova/.jshintrc @@ -0,0 +1,10 @@ +{ + "node": true + , "bitwise": true + , "undef": true + , "trailing": true + , "quotmark": true + , "indent": 4 + , "unused": "vars" + , "latedef": "nofunc" +} diff --git a/StoneIsland/platforms/android/cordova/Api.js b/StoneIsland/platforms/android/cordova/Api.js new file mode 100644 index 00000000..8e4711cb --- /dev/null +++ b/StoneIsland/platforms/android/cordova/Api.js @@ -0,0 +1,415 @@ +/** + 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, '..'); + + 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'), + // 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.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/xml/strings.xml'); + this.locations.manifest = path.join(self.root, 'app/src/main/AndroidManifest.xml'); + 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<PlatformApi>} 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<PlatformApi>} 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 <framework>'); + require('./lib/builders/builders').getBuilder('gradle').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 <framework>'); + require('./lib/builders/builders').getBuilder('gradle').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<Object[]>} 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; + 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; + 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<Requirement[]>} 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/StoneIsland/platforms/android/cordova/build b/StoneIsland/platforms/android/cordova/build index 3c3aee4e..222e84a0 100755 --- a/StoneIsland/platforms/android/cordova/build +++ b/StoneIsland/platforms/android/cordova/build @@ -19,23 +19,32 @@ under the License. */ -var build = require('./lib/build'), - reqs = require('./lib/check_reqs'), - args = process.argv; +var args = process.argv; +var Api = require('./Api'); +var nopt = require('nopt'); +var path = require('path'); // Support basic help commands -if(args[2] == '--help' || - args[2] == '/?' || - args[2] == '-h' || - args[2] == 'help' || - args[2] == '-help' || - args[2] == '/help') { - build.help(); -} else { - reqs.run().done(function() { - return build.run(args.slice(2)); - }, function(err) { - console.error(err); - process.exit(2); - }); -} +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/StoneIsland/platforms/android/cordova/build.bat b/StoneIsland/platforms/android/cordova/build.bat index 46e966af..46e966af 100755..100644 --- a/StoneIsland/platforms/android/cordova/build.bat +++ b/StoneIsland/platforms/android/cordova/build.bat diff --git a/StoneIsland/platforms/android/cordova/check_reqs.bat b/StoneIsland/platforms/android/cordova/check_reqs.bat index cb2c6f54..cb2c6f54 100755..100644 --- a/StoneIsland/platforms/android/cordova/check_reqs.bat +++ b/StoneIsland/platforms/android/cordova/check_reqs.bat diff --git a/StoneIsland/platforms/android/cordova/clean b/StoneIsland/platforms/android/cordova/clean index d9a7d490..22065cc5 100755 --- a/StoneIsland/platforms/android/cordova/clean +++ b/StoneIsland/platforms/android/cordova/clean @@ -19,26 +19,33 @@ under the License. */ -var build = require('./lib/build'), - reqs = require('./lib/check_reqs'), - args = process.argv; +var Api = require('./Api'); var path = require('path'); +var nopt = require('nopt'); // Support basic help commands -if(args[2] == '--help' || - args[2] == '/?' || - args[2] == '-h' || - args[2] == 'help' || - args[2] == '-help' || - args[2] == '/help') { +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); -} else { - reqs.run().done(function() { - return build.runClean(args.slice(2)); - }, function(err) { - console.error(err); - process.exit(2); - }); } + +// 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/StoneIsland/platforms/android/cordova/clean.bat b/StoneIsland/platforms/android/cordova/clean.bat index 445ef6e1..445ef6e1 100755..100644 --- a/StoneIsland/platforms/android/cordova/clean.bat +++ b/StoneIsland/platforms/android/cordova/clean.bat diff --git a/StoneIsland/platforms/android/cordova/defaults.xml b/StoneIsland/platforms/android/cordova/defaults.xml index 5286ab9c..5286ab9c 100755..100644 --- a/StoneIsland/platforms/android/cordova/defaults.xml +++ b/StoneIsland/platforms/android/cordova/defaults.xml diff --git a/StoneIsland/platforms/android/cordova/lib/Adb.js b/StoneIsland/platforms/android/cordova/lib/Adb.js new file mode 100644 index 00000000..84ae707e --- /dev/null +++ b/StoneIsland/platforms/android/cordova/lib/Adb.js @@ -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. +*/ + +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<String[]>} 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/StoneIsland/platforms/android/cordova/lib/AndroidManifest.js b/StoneIsland/platforms/android/cordova/lib/AndroidManifest.js new file mode 100644 index 00000000..8248f593 --- /dev/null +++ b/StoneIsland/platforms/android/cordova/lib/AndroidManifest.js @@ -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. +*/ + +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/StoneIsland/platforms/android/cordova/lib/AndroidProject.js b/StoneIsland/platforms/android/cordova/lib/AndroidProject.js new file mode 100644 index 00000000..fa1c6129 --- /dev/null +++ b/StoneIsland/platforms/android/cordova/lib/AndroidProject.js @@ -0,0 +1,210 @@ +/** + 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/StoneIsland/platforms/android/cordova/lib/AndroidStudio.js b/StoneIsland/platforms/android/cordova/lib/AndroidStudio.js new file mode 100644 index 00000000..335b334b --- /dev/null +++ b/StoneIsland/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', 'project.properties', 'platform_www']; + var androidStudioFiles = ['app', 'gradle', 'app/src/main/res']; + + // 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/StoneIsland/platforms/android/cordova/lib/appinfo.js b/StoneIsland/platforms/android/cordova/lib/appinfo.js deleted file mode 100755 index 080c2ba8..00000000 --- a/StoneIsland/platforms/android/cordova/lib/appinfo.js +++ /dev/null @@ -1,41 +0,0 @@ -#!/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 fs = require('fs'); -var cachedAppInfo = null; - -function readAppInfoFromManifest() { - var manifestPath = path.join(__dirname, '..', '..', 'AndroidManifest.xml'); - var manifestData = fs.readFileSync(manifestPath, {encoding:'utf8'}); - var packageName = /\bpackage\s*=\s*"(.+?)"/.exec(manifestData); - if (!packageName) throw new Error('Could not find package name within ' + manifestPath); - var activityTag = /<activity\b[\s\S]*<\/activity>/.exec(manifestData); - if (!activityTag) throw new Error('Could not find <activity> within ' + manifestPath); - var activityName = /\bandroid:name\s*=\s*"(.+?)"/.exec(activityTag); - if (!activityName) throw new Error('Could not find android:name within ' + manifestPath); - - return packageName[1] + '/.' + activityName[1]; -} - -exports.getActivityName = function() { - return (cachedAppInfo = cachedAppInfo || readAppInfoFromManifest()); -}; diff --git a/StoneIsland/platforms/android/cordova/lib/build.js b/StoneIsland/platforms/android/cordova/lib/build.js index aa9f3d01..bd613da2 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/build.js +++ b/StoneIsland/platforms/android/cordova/lib/build.js @@ -19,477 +19,68 @@ under the License. */ -/* jshint sub:true */ - -var shell = require('shelljs'), - spawn = require('./spawn'), - Q = require('q'), +var Q = require('q'), path = require('path'), fs = require('fs'), - os = require('os'), - ROOT = path.join(__dirname, '..', '..'); -var check_reqs = require('./check_reqs'); -var exec = require('./exec'); - + nopt = require('nopt'); -var SIGNING_PROPERTIES = '-signing.properties'; -var MARKER = 'YOUR CHANGES WILL BE ERASED!'; -var TEMPLATE = - '# This file is automatically generated.\n' + - '# Do not modify this file -- ' + MARKER + '\n'; - -function findApks(directory) { - var ret = []; - if (fs.existsSync(directory)) { - fs.readdirSync(directory).forEach(function(p) { - if (path.extname(p) == '.apk') { - ret.push(path.join(directory, p)); - } - }); - } - return ret; -} +var Adb = require('./Adb'); -function sortFilesByDate(files) { - return files.map(function(p) { - return { p: p, t: fs.statSync(p).mtime }; - }).sort(function(a, b) { - var timeDiff = b.t - a.t; - return timeDiff === 0 ? a.p.length - b.p.length : timeDiff; - }).map(function(p) { return p.p; }); -} +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 isAutoGenerated(file) { - if(fs.existsSync(file)) { - var fileContents = fs.readFileSync(file, 'utf8'); - return fileContents.indexOf(MARKER) > 0; - } - return false; -} +function parseOpts(options, resolvedTarget, projectRoot) { + options = options || {}; + options.argv = nopt({ + gradle: Boolean, + ant: Boolean, + prepenv: Boolean, + versionCode: String, + minSdkVersion: String, + gradleArg: [String, Array], + keystore: path, + alias: String, + storePassword: String, + password: String, + keystoreType: String + }, {}, options.argv, 0); -function findOutputApksHelper(dir, build_type, arch) { - var ret = findApks(dir).filter(function(candidate) { - // Need to choose between release and debug .apk. - if (build_type === 'debug') { - return /-debug/.exec(candidate) && !/-unaligned|-unsigned/.exec(candidate); - } - if (build_type === 'release') { - return /-release/.exec(candidate) && !/-unaligned/.exec(candidate); - } - return true; - }); - ret = sortFilesByDate(ret); - if (ret.length === 0) { - return ret; - } - // Assume arch-specific build if newest apk has -x86 or -arm. - var archSpecific = !!/-x86|-arm/.exec(ret[0]); - // And show only arch-specific ones (or non-arch-specific) - ret = ret.filter(function(p) { - /*jshint -W018 */ - return !!/-x86|-arm/.exec(p) == archSpecific; - /*jshint +W018 */ - }); - if (archSpecific && ret.length > 1) { - ret = ret.filter(function(p) { - return p.indexOf('-' + arch) != -1; - }); - } - - return ret; -} - -function hasCustomRules() { - return fs.existsSync(path.join(ROOT, 'custom_rules.xml')); -} - -function extractRealProjectNameFromManifest(projectPath) { - var manifestPath = path.join(projectPath, 'AndroidManifest.xml'); - var manifestData = fs.readFileSync(manifestPath, 'utf8'); - var m = /<manifest[\s\S]*?package\s*=\s*"(.*?)"/i.exec(manifestData); - if (!m) { - throw new Error('Could not find package name in ' + manifestPath); - } - - var packageName=m[1]; - var lastDotIndex = packageName.lastIndexOf('.'); - return packageName.substring(lastDotIndex + 1); -} - -function extractProjectNameFromManifest(projectPath) { - var manifestPath = path.join(projectPath, 'AndroidManifest.xml'); - var manifestData = fs.readFileSync(manifestPath, 'utf8'); - var m = /<activity[\s\S]*?android:name\s*=\s*"(.*?)"/i.exec(manifestData); - if (!m) { - throw new Error('Could not find activity name in ' + manifestPath); - } - return m[1]; -} - -function findAllUniq(data, r) { - var s = {}; - var m; - while ((m = r.exec(data))) { - s[m[1]] = 1; - } - return Object.keys(s); -} - -function readProjectProperties() { - var data = fs.readFileSync(path.join(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) + var ret = { + buildType: options.release ? 'release' : 'debug', + buildMethod: process.env.ANDROID_BUILD || 'gradle', + prepEnv: options.argv.prepenv, + arch: resolvedTarget && resolvedTarget.arch, + extraArgs: [] }; -} - -var builders = { - ant: { - getArgs: function(cmd, opts) { - var args = [cmd, '-f', path.join(ROOT, 'build.xml')]; - // custom_rules.xml is required for incremental builds. - if (hasCustomRules()) { - args.push('-Dout.dir=ant-build', '-Dgen.absolute.dir=ant-gen'); - } - if(opts.packageInfo) { - args.push('-propertyfile=' + path.join(ROOT, opts.buildType + SIGNING_PROPERTIES)); - } - return args; - }, - prepEnv: function(opts) { - return check_reqs.check_ant() - .then(function() { - // Copy in build.xml on each build so that: - // A) we don't require the Android SDK at project creation time, and - // B) we always use the SDK's latest version of it. - var sdkDir = process.env['ANDROID_HOME']; - var buildTemplate = fs.readFileSync(path.join(sdkDir, 'tools', 'lib', 'build.template'), 'utf8'); - function writeBuildXml(projectPath) { - var newData = buildTemplate.replace('PROJECT_NAME', extractProjectNameFromManifest(ROOT)); - fs.writeFileSync(path.join(projectPath, 'build.xml'), newData); - if (!fs.existsSync(path.join(projectPath, 'local.properties'))) { - fs.writeFileSync(path.join(projectPath, 'local.properties'), TEMPLATE); - } - } - writeBuildXml(ROOT); - var propertiesObj = readProjectProperties(); - var subProjects = propertiesObj.libs; - for (var i = 0; i < subProjects.length; ++i) { - writeBuildXml(path.join(ROOT, subProjects[i])); - } - if (propertiesObj.systemLibs.length > 0) { - throw new Error('Project contains at least one plugin that requires a system library. This is not supported with ANT. Please build using gradle.'); - } + if (options.argv.ant || options.argv.gradle) + ret.buildMethod = options.argv.ant ? 'ant' : 'gradle'; - var propertiesFile = opts.buildType + SIGNING_PROPERTIES; - var propertiesFilePath = path.join(ROOT, propertiesFile); - if (opts.packageInfo) { - fs.writeFileSync(propertiesFilePath, TEMPLATE + opts.packageInfo.toProperties()); - } else if(isAutoGenerated(propertiesFilePath)) { - shell.rm('-f', propertiesFilePath); - } - }); - }, + if (options.nobuild) ret.buildMethod = 'none'; - /* - * Builds the project with ant. - * Returns a promise. - */ - build: function(opts) { - // Without our custom_rules.xml, we need to clean before building. - var ret = Q(); - if (!hasCustomRules()) { - // clean will call check_ant() for us. - ret = this.clean(opts); - } + if (options.argv.versionCode) + ret.extraArgs.push('-PcdvVersionCode=' + options.argv.versionCode); - var args = this.getArgs(opts.buildType == 'debug' ? 'debug' : 'release', opts); - return check_reqs.check_ant() - .then(function() { - console.log('Executing: ant ' + args.join(' ')); - return spawn('ant', args); - }); - }, - - clean: function(opts) { - var args = this.getArgs('clean', opts); - return check_reqs.check_ant() - .then(function() { - return spawn('ant', args); - }); - }, + if (options.argv.minSdkVersion) + ret.extraArgs.push('-PcdvMinSdkVersion=' + options.argv.minSdkVersion); - findOutputApks: function(build_type) { - var binDir = path.join(ROOT, hasCustomRules() ? 'ant-build' : 'bin'); - return findOutputApksHelper(binDir, build_type, null); - } - }, - gradle: { - getArgs: function(cmd, opts) { - if (cmd == 'release') { - cmd = 'cdvBuildRelease'; - } else if (cmd == 'debug') { - cmd = 'cdvBuildDebug'; - } - var args = [cmd, '-b', path.join(ROOT, 'build.gradle')]; - if (opts.arch) { - args.push('-PcdvBuildArch=' + opts.arch); - } - - // 10 seconds -> 6 seconds - args.push('-Dorg.gradle.daemon=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; - }, - - // Makes the project buildable, minus the gradle wrapper. - prepBuildFiles: function() { - var projectPath = ROOT; - // Update the version of build.gradle in each dependent library. - var pluginBuildGradle = path.join(projectPath, 'cordova', 'lib', 'plugin-build.gradle'); - var propertiesObj = readProjectProperties(); - var subProjects = propertiesObj.libs; - for (var i = 0; i < subProjects.length; ++i) { - if (subProjects[i] !== 'CordovaLib') { - shell.cp('-f', pluginBuildGradle, path.join(ROOT, subProjects[i], 'build.gradle')); - } - } - - var name = extractRealProjectNameFromManifest(ROOT); - //Remove the proj.id/name- prefix from projects: https://issues.apache.org/jira/browse/CB-9149 - var settingsGradlePaths = subProjects.map(function(p){ - var realDir=p.replace(/[/\\]/g, ':'); - var libName=realDir.replace(name+'-',''); - var str='include ":'+libName+'"\n'; - if(realDir.indexOf(name+'-')!==-1) - str+='project(":'+libName+'").projectDir = new File("'+p+'")\n'; - return str; - }); - - // Write the settings.gradle file. - fs.writeFileSync(path.join(projectPath, 'settings.gradle'), - '// GENERATED FILE - DO NOT EDIT\n' + - 'include ":"\n' + settingsGradlePaths.join('')); - // Update dependencies within build.gradle. - var buildGradle = fs.readFileSync(path.join(projectPath, 'build.gradle'), 'utf8'); - var depsList = ''; - subProjects.forEach(function(p) { - var libName=p.replace(/[/\\]/g, ':').replace(name+'-',''); - depsList += ' debugCompile project(path: "' + libName + '", configuration: "debug")\n'; - depsList += ' releaseCompile project(path: "' + libName + '", configuration: "release")\n'; - }); - // For why we do this mapping: https://issues.apache.org/jira/browse/CB-8390 - var SYSTEM_LIBRARY_MAPPINGS = [ - [/^\/?extras\/android\/support\/(.*)$/, 'com.android.support:support-$1:+'], - [/^\/?google\/google_play_services\/libproject\/google-play-services_lib\/?$/, 'com.google.android.gms:play-services:+'] - ]; - propertiesObj.systemLibs.forEach(function(p) { - var mavenRef; - // It's already in gradle form if it has two ':'s - if (/:.*:/.exec(p)) { - mavenRef = p; - } else { - for (var i = 0; i < SYSTEM_LIBRARY_MAPPINGS.length; ++i) { - var pair = SYSTEM_LIBRARY_MAPPINGS[i]; - if (pair[0].exec(p)) { - mavenRef = p.replace(pair[0], pair[1]); - break; - } - } - if (!mavenRef) { - throw new Error('Unsupported system library (does not work with gradle): ' + p); - } - } - depsList += ' compile "' + mavenRef + '"\n'; - }); - buildGradle = buildGradle.replace(/(SUB-PROJECT DEPENDENCIES START)[\s\S]*(\/\/ SUB-PROJECT DEPENDENCIES END)/, '$1\n' + depsList + ' $2'); - var includeList = ''; - propertiesObj.gradleIncludes.forEach(function(includePath) { - includeList += 'apply from: "' + includePath + '"\n'; - }); - buildGradle = buildGradle.replace(/(PLUGIN GRADLE EXTENSIONS START)[\s\S]*(\/\/ PLUGIN GRADLE EXTENSIONS END)/, '$1\n' + includeList + '$2'); - fs.writeFileSync(path.join(projectPath, 'build.gradle'), buildGradle); - }, - - prepEnv: function(opts) { - var self = this; - return check_reqs.check_gradle() - .then(function() { - return self.prepBuildFiles(); - }).then(function() { - // Copy the gradle wrapper on each build so that: - // A) we don't require the Android SDK at project creation time, and - // B) we always use the SDK's latest version of it. - var projectPath = ROOT; - // check_reqs ensures that this is set. - var sdkDir = process.env['ANDROID_HOME']; - var wrapperDir = path.join(sdkDir, 'tools', 'templates', 'gradle', 'wrapper'); - if (process.platform == 'win32') { - shell.rm('-f', path.join(projectPath, 'gradlew.bat')); - shell.cp(path.join(wrapperDir, 'gradlew.bat'), projectPath); - } else { - shell.rm('-f', path.join(projectPath, 'gradlew')); - shell.cp(path.join(wrapperDir, 'gradlew'), projectPath); - } - shell.rm('-rf', path.join(projectPath, 'gradle', 'wrapper')); - shell.mkdir('-p', path.join(projectPath, 'gradle')); - shell.cp('-r', path.join(wrapperDir, 'gradle', 'wrapper'), path.join(projectPath, 'gradle')); - - // If the gradle distribution URL is set, make sure it points to version we want. - // If it's not set, do nothing, assuming that we're using a future version of gradle that we don't want to mess with. - // For some reason, using ^ and $ don't work. This does the job, though. - var distributionUrlRegex = /distributionUrl.*zip/; - var distributionUrl = 'distributionUrl=http\\://services.gradle.org/distributions/gradle-2.2.1-all.zip'; - var gradleWrapperPropertiesPath = path.join(projectPath, 'gradle', 'wrapper', 'gradle-wrapper.properties'); - shell.chmod('u+w', gradleWrapperPropertiesPath); - shell.sed('-i', distributionUrlRegex, distributionUrl, gradleWrapperPropertiesPath); - - var propertiesFile = opts.buildType + SIGNING_PROPERTIES; - var propertiesFilePath = path.join(ROOT, propertiesFile); - if (opts.packageInfo) { - fs.writeFileSync(propertiesFilePath, TEMPLATE + opts.packageInfo.toProperties()); - } else if (isAutoGenerated(propertiesFilePath)) { - shell.rm('-f', propertiesFilePath); - } - }); - }, - - /* - * Builds the project with gradle. - * Returns a promise. - */ - build: function(opts) { - var wrapper = path.join(ROOT, 'gradlew'); - var args = this.getArgs(opts.buildType == 'debug' ? 'debug' : 'release', opts); - return Q().then(function() { - console.log('Running: ' + wrapper + ' ' + args.join(' ')); - return spawn(wrapper, args); - }); - }, - - clean: function(opts) { - var builder = this; - var wrapper = path.join(ROOT, 'gradlew'); - var args = builder.getArgs('clean', opts); - return Q().then(function() { - console.log('Running: ' + wrapper + ' ' + args.join(' ')); - return spawn(wrapper, args); - }); - }, - - findOutputApks: function(build_type, arch) { - var binDir = path.join(ROOT, 'build', 'outputs', 'apk'); - return findOutputApksHelper(binDir, build_type, arch); - } - }, - - none: { - prepEnv: function() { - return Q(); - }, - build: function() { - console.log('Skipping build...'); - return Q(null); - }, - clean: function() { - return Q(); - }, - findOutputApks: function(build_type, arch) { - return sortFilesByDate(builders.ant.findOutputApks(build_type, arch).concat(builders.gradle.findOutputApks(build_type, arch))); - } + if (options.argv.gradleArg) { + ret.extraArgs = ret.extraArgs.concat(options.argv.gradleArg); } -}; -module.exports.isBuildFlag = function(flag) { - return /^--(debug|release|ant|gradle|nobuild|versionCode=|minSdkVersion=|gradleArg=|keystore=|alias=|password=|storePassword=|keystoreType=|buildConfig=)/.exec(flag); -}; + var packageArgs = {}; -function parseOpts(options, resolvedTarget) { - // Backwards-compatibility: Allow a single string argument - if (typeof options == 'string') options = [options]; + if (options.argv.keystore) + packageArgs.keystore = path.relative(projectRoot, path.resolve(options.argv.keystore)); - var ret = { - buildType: 'debug', - buildMethod: process.env['ANDROID_BUILD'] || 'gradle', - arch: null, - extraArgs: [] - }; + ['alias','storePassword','password','keystoreType'].forEach(function (flagName) { + if (options.argv[flagName]) + packageArgs[flagName] = options.argv[flagName]; + }); - var multiValueArgs = { - 'versionCode': true, - 'minSdkVersion': true, - 'gradleArg': true, - 'keystore' : true, - 'alias' : true, - 'password' : true, - 'storePassword' : true, - 'keystoreType' : true, - 'buildConfig' : true - }; - var packageArgs = {}; - var buildConfig; - // Iterate through command line options - for (var i=0; options && (i < options.length); ++i) { - if (/^--/.exec(options[i])) { - var keyValue = options[i].substring(2).split('='); - var flagName = keyValue.shift(); - var flagValue = keyValue.join('='); - if (multiValueArgs[flagName] && !flagValue) { - flagValue = options[i + 1]; - ++i; - } - switch(flagName) { - case 'debug': - case 'release': - ret.buildType = flagName; - break; - case 'ant': - case 'gradle': - ret.buildMethod = flagName; - break; - case 'device': - case 'emulator': - // Don't need to do anything special to when building for device vs emulator. - // iOS uses this flag to switch on architecture. - break; - case 'prepenv' : - ret.prepEnv = true; - break; - case 'nobuild' : - ret.buildMethod = 'none'; - break; - case 'versionCode': - ret.extraArgs.push('-PcdvVersionCode=' + flagValue); - break; - case 'minSdkVersion': - ret.extraArgs.push('-PcdvMinSdkVersion=' + flagValue); - break; - case 'gradleArg': - ret.extraArgs.push(flagValue); - break; - case 'keystore': - packageArgs.keystore = path.relative(ROOT, path.resolve(flagValue)); - break; - case 'alias': - case 'storePassword': - case 'password': - case 'keystoreType': - packageArgs[flagName] = flagValue; - break; - case 'buildConfig': - buildConfig = flagValue; - break; - default : - console.warn('Build option --\'' + flagName + '\' not recognized (ignoring).'); - } - } else { - console.warn('Build option \'' + options[i] + '\' not recognized (ignoring).'); - } - } + 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. @@ -497,16 +88,17 @@ function parseOpts(options, resolvedTarget) { if (!fs.existsSync(buildConfig)) { throw new Error('Specified build config file does not exist: ' + buildConfig); } - console.log('Reading build config file: '+ path.resolve(buildConfig)); - var config = JSON.parse(fs.readFileSync(buildConfig, 'utf8')); + 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(path.isAbsolute(androidInfo.keystore)) { - packageArgs.keystore = androidInfo.keystore; - } else { - packageArgs.keystore = path.relative(ROOT, path.join(path.dirname(buildConfig), androidInfo.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){ @@ -514,6 +106,7 @@ function parseOpts(options, resolvedTarget) { }); } } + if (packageArgs.keystore && packageArgs.alias) { ret.packageInfo = new PackageInfo(packageArgs.keystore, packageArgs.alias, packageArgs.storePassword, packageArgs.password, packageArgs.keystoreType); @@ -521,10 +114,9 @@ function parseOpts(options, resolvedTarget) { if(!ret.packageInfo) { if(Object.keys(packageArgs).length > 0) { - console.warn('\'keystore\' and \'alias\' need to be specified to generate a signed archive.'); + events.emit('warn', '\'keystore\' and \'alias\' need to be specified to generate a signed archive.'); } } - ret.arch = resolvedTarget && resolvedTarget.arch; return ret; } @@ -534,41 +126,39 @@ function parseOpts(options, resolvedTarget) { * Returns a promise. */ module.exports.runClean = function(options) { - var opts = parseOpts(options); - var builder = builders[opts.buildMethod]; + var opts = parseOpts(options, null, this.root); + var builder = builders.getBuilder(opts.buildMethod); return builder.prepEnv(opts) .then(function() { return builder.clean(opts); - }).then(function() { - shell.rm('-rf', path.join(ROOT, 'out')); - - ['debug', 'release'].forEach(function(config) { - var propertiesFilePath = path.join(ROOT, config + SIGNING_PROPERTIES); - if(isAutoGenerated(propertiesFilePath)){ - shell.rm('-f', propertiesFilePath); - } - }); }); }; -/* - * Builds the project with the specifed options - * Returns a promise. +/** + * 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<Object>} Promise, resolved with built packages + * information. */ module.exports.run = function(options, optResolvedTarget) { - var opts = parseOpts(options, optResolvedTarget); - var builder = builders[opts.buildMethod]; + var opts = parseOpts(options, optResolvedTarget, this.root); + var builder = builders.getBuilder(opts.buildMethod); return builder.prepEnv(opts) .then(function() { if (opts.prepEnv) { - console.log('Build file successfully prepared.'); + events.emit('verbose', 'Build file successfully prepared.'); return; } return builder.build(opts) .then(function() { var apkPaths = builder.findOutputApks(opts.buildType, opts.arch); - console.log('Built the following apk(s):'); - console.log(' ' + apkPaths.join('\n ')); + events.emit('log', 'Built the following apk(s): \n\t' + apkPaths.join('\n\t')); return { apkPaths: apkPaths, buildType: opts.buildType, @@ -578,46 +168,38 @@ module.exports.run = function(options, optResolvedTarget) { }); }; -// Called by plugman after installing plugins, and by create script after creating project. -module.exports.prepBuildFiles = function() { - var builder = builders['gradle']; - return builder.prepBuildFiles(); -}; - /* * Detects the architecture of a device/emulator * Returns "arm" or "x86". */ module.exports.detectArchitecture = function(target) { function helper() { - return exec('adb -s ' + target + ' shell cat /proc/cpuinfo', os.tmpdir()) + return Adb.shell(target, 'cat /proc/cpuinfo') .then(function(output) { - if (/intel/i.exec(output)) { - return 'x86'; - } - return 'arm'; + 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, 'Device communication timed out. Try unplugging & replugging the device.') + 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. - return exec('killall adb') + events.emit('verbose', 'adb timed out while detecting device/emulator architecture. Killing adb and trying again.'); + return spawn('killall', ['adb']) .then(function() { - console.log('adb seems hung. retrying.'); return helper() .then(null, function() { // The double kill is sadly often necessary, at least on mac. - console.log('Now device not found... restarting adb again.'); - return exec('killall adb') + 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('USB is flakey. Try unplugging & replugging the device.'); + return Q.reject(new CordovaError('adb timed out a third time while detecting device/emulator architecture. Try unplugging & replugging the device.')); }); }); }); @@ -632,16 +214,18 @@ module.exports.detectArchitecture = function(target) { 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(p); + return /-debug/.exec(apkName); } - return !/-debug/.exec(p); + return !/-debug/.exec(apkName); }); var archPattern = new RegExp('-' + arch); var hasArchPattern = /-x86|-arm/; for (var i = 0; i < paths.length; ++i) { - if (hasArchPattern.exec(paths[i])) { - if (archPattern.exec(paths[i])) { + var apkName = path.basename(paths[i]); + if (hasArchPattern.exec(apkName)) { + if (archPattern.exec(apkName)) { return paths[i]; } } else { @@ -695,7 +279,7 @@ PackageInfo.prototype = { }; module.exports.help = function() { - console.log('Usage: ' + path.relative(process.cwd(), path.join(ROOT, 'cordova', 'build')) + ' [flags] [Signed APK flags]'); + 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'); diff --git a/StoneIsland/platforms/android/cordova/lib/builders/AntBuilder.js b/StoneIsland/platforms/android/cordova/lib/builders/AntBuilder.js new file mode 100644 index 00000000..4e0f71ab --- /dev/null +++ b/StoneIsland/platforms/android/cordova/lib/builders/AntBuilder.js @@ -0,0 +1,156 @@ +/* + 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 path = require('path'); +var util = require('util'); +var shell = require('shelljs'); +var spawn = require('cordova-common').superspawn.spawn; +var CordovaError = require('cordova-common').CordovaError; +var check_reqs = require('../check_reqs'); + +var SIGNING_PROPERTIES = '-signing.properties'; +var MARKER = 'YOUR CHANGES WILL BE ERASED!'; +var TEMPLATE = + '# This file is automatically generated.\n' + + '# Do not modify this file -- ' + MARKER + '\n'; + +var GenericBuilder = require('./GenericBuilder'); + +function AntBuilder (projectRoot) { + GenericBuilder.call(this, projectRoot); + + this.binDirs = {ant: this.binDirs.ant}; +} + +util.inherits(AntBuilder, GenericBuilder); + +AntBuilder.prototype.getArgs = function(cmd, opts) { + var args = [cmd, '-f', path.join(this.root, 'build.xml')]; + // custom_rules.xml is required for incremental builds. + if (hasCustomRules(this.root)) { + args.push('-Dout.dir=ant-build', '-Dgen.absolute.dir=ant-gen'); + } + if(opts.packageInfo) { + args.push('-propertyfile=' + path.join(this.root, opts.buildType + SIGNING_PROPERTIES)); + } + return args; +}; + +AntBuilder.prototype.prepEnv = function(opts) { + var self = this; + return check_reqs.check_ant() + .then(function() { + // Copy in build.xml on each build so that: + // A) we don't require the Android SDK at project creation time, and + // B) we always use the SDK's latest version of it. + /*jshint -W069 */ + var sdkDir = process.env['ANDROID_HOME']; + /*jshint +W069 */ + var buildTemplate = fs.readFileSync(path.join(sdkDir, 'tools', 'lib', 'build.template'), 'utf8'); + function writeBuildXml(projectPath) { + var newData = buildTemplate.replace('PROJECT_NAME', self.extractRealProjectNameFromManifest()); + fs.writeFileSync(path.join(projectPath, 'build.xml'), newData); + if (!fs.existsSync(path.join(projectPath, 'local.properties'))) { + fs.writeFileSync(path.join(projectPath, 'local.properties'), TEMPLATE); + } + } + writeBuildXml(self.root); + var propertiesObj = self.readProjectProperties(); + var subProjects = propertiesObj.libs; + for (var i = 0; i < subProjects.length; ++i) { + writeBuildXml(path.join(self.root, subProjects[i])); + } + if (propertiesObj.systemLibs.length > 0) { + throw new CordovaError('Project contains at least one plugin that requires a system library. This is not supported with ANT. Use gradle instead.'); + } + + var propertiesFile = opts.buildType + SIGNING_PROPERTIES; + var propertiesFilePath = path.join(self.root, propertiesFile); + if (opts.packageInfo) { + fs.writeFileSync(propertiesFilePath, TEMPLATE + opts.packageInfo.toProperties()); + } else if(isAutoGenerated(propertiesFilePath)) { + shell.rm('-f', propertiesFilePath); + } + }); +}; + +/* + * Builds the project with ant. + * Returns a promise. + */ +AntBuilder.prototype.build = function(opts) { + // Without our custom_rules.xml, we need to clean before building. + var ret = Q(); + if (!hasCustomRules(this.root)) { + // clean will call check_ant() for us. + ret = this.clean(opts); + } + + var args = this.getArgs(opts.buildType == 'debug' ? 'debug' : 'release', opts); + return check_reqs.check_ant() + .then(function() { + return spawn('ant', args, {stdio: 'pipe'}); + }).progress(function (stdio){ + if (stdio.stderr) { + process.stderr.write(stdio.stderr); + } else { + process.stdout.write(stdio.stdout); + } + }).catch(function (error) { + if (error.toString().indexOf('Unable to resolve project target') >= 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); + }); +}; + +AntBuilder.prototype.clean = function(opts) { + var args = this.getArgs('clean', opts); + var self = this; + return check_reqs.check_ant() + .then(function() { + return spawn('ant', args, {stdio: 'inherit'}); + }) + .then(function () { + shell.rm('-rf', path.join(self.root, 'out')); + + ['debug', 'release'].forEach(function(config) { + var propertiesFilePath = path.join(self.root, config + SIGNING_PROPERTIES); + if(isAutoGenerated(propertiesFilePath)){ + shell.rm('-f', propertiesFilePath); + } + }); + }); +}; + +module.exports = AntBuilder; + +function hasCustomRules(projectRoot) { + return fs.existsSync(path.join(projectRoot, 'custom_rules.xml')); +} + +function isAutoGenerated(file) { + return fs.existsSync(file) && fs.readFileSync(file, 'utf8').indexOf(MARKER) > 0; +} diff --git a/StoneIsland/platforms/android/cordova/lib/builders/GenericBuilder.js b/StoneIsland/platforms/android/cordova/lib/builders/GenericBuilder.js new file mode 100644 index 00000000..362da431 --- /dev/null +++ b/StoneIsland/platforms/android/cordova/lib/builders/GenericBuilder.js @@ -0,0 +1,147 @@ +/* + 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 path = require('path'); +var shell = require('shelljs'); +var events = require('cordova-common').events; +var CordovaError = require('cordova-common').CordovaError; + +function GenericBuilder (projectDir) { + this.root = projectDir || path.resolve(__dirname, '../../..'); + this.binDirs = { + ant: path.join(this.root, hasCustomRules(this.root) ? 'ant-build' : 'bin'), + gradle: path.join(this.root, 'build', 'outputs', 'apk') + }; +} + +function hasCustomRules(projectRoot) { + return fs.existsSync(path.join(projectRoot, 'custom_rules.xml')); +} + +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); +}; + +GenericBuilder.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) + }; +}; + +GenericBuilder.prototype.extractRealProjectNameFromManifest = function () { + var manifestPath = path.join(this.root, 'AndroidManifest.xml'); + var manifestData = fs.readFileSync(manifestPath, 'utf8'); + var m = /<manifest[\s\S]*?package\s*=\s*"(.*?)"/i.exec(manifestData); + if (!m) { + throw new CordovaError('Could not find package name in ' + manifestPath); + } + + var packageName=m[1]; + var lastDotIndex = packageName.lastIndexOf('.'); + return packageName.substring(lastDotIndex + 1); +}; + +module.exports = GenericBuilder; + +function apkSorter(fileA, fileB) { + // De-prioritize unsigned builds + var unsignedRE = /-unsigned/; + if (unsignedRE.exec(fileA)) { + return 1; + } else if (unsignedRE.exec(fileB)) { + return -1; + } + + var timeDiff = fs.statSync(fileA).mtime - fs.statSync(fileB).mtime; + return timeDiff === 0 ? fileA.length - fileB.length : timeDiff; +} + +function findOutputApksHelper(dir, build_type, arch) { + var shellSilent = shell.config.silent; + shell.config.silent = true; + + var ret = shell.ls(path.join(dir, '*.apk')) + .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/StoneIsland/platforms/android/cordova/lib/builders/GradleBuilder.js b/StoneIsland/platforms/android/cordova/lib/builders/GradleBuilder.js new file mode 100644 index 00000000..f415646e --- /dev/null +++ b/StoneIsland/platforms/android/cordova/lib/builders/GradleBuilder.js @@ -0,0 +1,266 @@ +/* + 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 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; +}; + +// Makes the project buildable, minus the gradle wrapper. +GradleBuilder.prototype.prepBuildFiles = function() { + // Update the version of build.gradle in each dependent library. + var pluginBuildGradle = path.join(this.root, 'cordova', 'lib', 'plugin-build.gradle'); + var propertiesObj = this.readProjectProperties(); + var subProjects = propertiesObj.libs; + var checkAndCopy = function(subProject, root) { + var subProjectGradle = path.join(root, subProject, 'build.gradle'); + // This is the future-proof way of checking if a file exists + // This must be synchronous to satisfy a Travis test + try { + fs.accessSync(subProjectGradle, fs.F_OK); + } catch (e) { + shell.cp('-f', pluginBuildGradle, subProjectGradle); + } + }; + for (var i = 0; i < subProjects.length; ++i) { + if (subProjects[i] !== 'CordovaLib') { + checkAndCopy(subProjects[i], this.root); + } + } + var name = this.extractRealProjectNameFromManifest(); + //Remove the proj.id/name- prefix from projects: https://issues.apache.org/jira/browse/CB-9149 + var settingsGradlePaths = subProjects.map(function(p){ + var realDir=p.replace(/[/\\]/g, ':'); + var libName=realDir.replace(name+'-',''); + var str='include ":'+libName+'"\n'; + if(realDir.indexOf(name+'-')!==-1) + str+='project(":'+libName+'").projectDir = new File("'+p+'")\n'; + return str; + }); + + // Write the settings.gradle file. + fs.writeFileSync(path.join(this.root, 'settings.gradle'), + '// GENERATED FILE - DO NOT EDIT\n' + + 'include ":"\n' + settingsGradlePaths.join('')); + // Update dependencies within build.gradle. + var buildGradle = fs.readFileSync(path.join(this.root, 'build.gradle'), 'utf8'); + var depsList = ''; + var root = this.root; + var insertExclude = function(p) { + var gradlePath = path.join(root, p, 'build.gradle'); + var projectGradleFile = fs.readFileSync(gradlePath, 'utf-8'); + if(projectGradleFile.indexOf('CordovaLib') != -1) { + depsList += '{\n exclude module:("CordovaLib")\n }\n'; + } + else { + depsList +='\n'; + } + }; + subProjects.forEach(function(p) { + console.log('Subproject Path: ' + p); + var libName=p.replace(/[/\\]/g, ':').replace(name+'-',''); + depsList += ' debugCompile(project(path: "' + libName + '", configuration: "debug"))'; + insertExclude(p); + depsList += ' releaseCompile(project(path: "' + libName + '", configuration: "release"))'; + insertExclude(p); + }); + // For why we do this mapping: https://issues.apache.org/jira/browse/CB-8390 + var SYSTEM_LIBRARY_MAPPINGS = [ + [/^\/?extras\/android\/support\/(.*)$/, 'com.android.support:support-$1:+'], + [/^\/?google\/google_play_services\/libproject\/google-play-services_lib\/?$/, 'com.google.android.gms:play-services:+'] + ]; + propertiesObj.systemLibs.forEach(function(p) { + var mavenRef; + // It's already in gradle form if it has two ':'s + if (/:.*:/.exec(p)) { + mavenRef = p; + } else { + for (var i = 0; i < SYSTEM_LIBRARY_MAPPINGS.length; ++i) { + var pair = SYSTEM_LIBRARY_MAPPINGS[i]; + if (pair[0].exec(p)) { + mavenRef = p.replace(pair[0], pair[1]); + break; + } + } + if (!mavenRef) { + throw new CordovaError('Unsupported system library (does not work with gradle): ' + p); + } + } + depsList += ' compile "' + mavenRef + '"\n'; + }); + buildGradle = buildGradle.replace(/(SUB-PROJECT DEPENDENCIES START)[\s\S]*(\/\/ SUB-PROJECT DEPENDENCIES END)/, '$1\n' + depsList + ' $2'); + var includeList = ''; + propertiesObj.gradleIncludes.forEach(function(includePath) { + includeList += 'apply from: "' + includePath + '"\n'; + }); + buildGradle = buildGradle.replace(/(PLUGIN GRADLE EXTENSIONS START)[\s\S]*(\/\/ PLUGIN GRADLE EXTENSIONS END)/, '$1\n' + includeList + '$2'); + fs.writeFileSync(path.join(this.root, 'build.gradle'), buildGradle); +}; + +GradleBuilder.prototype.prepEnv = function(opts) { + var self = this; + return check_reqs.check_gradle() + .then(function() { + return self.prepBuildFiles(); + }).then(function() { + // Copy the gradle wrapper on each build so that: + // A) we don't require the Android SDK at project creation time, and + // B) we always use the SDK's latest version of it. + // check_reqs ensures that this is set. + /*jshint -W069 */ + var sdkDir = process.env['ANDROID_HOME']; + /*jshint +W069 */ + var wrapperDir = path.join(sdkDir, 'tools', 'templates', 'gradle', 'wrapper'); + if (process.platform == 'win32') { + shell.rm('-f', path.join(self.root, 'gradlew.bat')); + shell.cp(path.join(wrapperDir, 'gradlew.bat'), self.root); + } else { + shell.rm('-f', path.join(self.root, 'gradlew')); + shell.cp(path.join(wrapperDir, 'gradlew'), self.root); + } + shell.rm('-rf', path.join(self.root, 'gradle', 'wrapper')); + shell.mkdir('-p', path.join(self.root, 'gradle')); + shell.cp('-r', path.join(wrapperDir, 'gradle', 'wrapper'), path.join(self.root, 'gradle')); + + // If the gradle distribution URL is set, make sure it points to version we want. + // If it's not set, do nothing, assuming that we're using a future version of gradle that we don't want to mess with. + // For some reason, using ^ and $ don't work. This does the job, though. + var distributionUrlRegex = /distributionUrl.*zip/; + /*jshint -W069 */ + var distributionUrl = process.env['CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL'] || 'https\\://services.gradle.org/distributions/gradle-2.14.1-all.zip'; + /*jshint +W069 */ + var gradleWrapperPropertiesPath = path.join(self.root, 'gradle', 'wrapper', 'gradle-wrapper.properties'); + shell.chmod('u+w', gradleWrapperPropertiesPath); + shell.sed('-i', distributionUrlRegex, 'distributionUrl='+distributionUrl, gradleWrapperPropertiesPath); + + var propertiesFile = opts.buildType + SIGNING_PROPERTIES; + var propertiesFilePath = path.join(self.root, propertiesFile); + if (opts.packageInfo) { + fs.writeFileSync(propertiesFilePath, TEMPLATE + opts.packageInfo.toProperties()); + } else if (isAutoGenerated(propertiesFilePath)) { + shell.rm('-f', propertiesFilePath); + } + }); +}; + +/* + * Builds the project with gradle. + * Returns a promise. + */ +GradleBuilder.prototype.build = function(opts) { + var wrapper = path.join(this.root, 'gradlew'); + var args = this.getArgs(opts.buildType == 'debug' ? 'debug' : 'release', opts); + + return spawn(wrapper, args, {stdio: 'pipe'}) + .progress(function (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); + } + }).catch(function (error) { + if (error.toString().indexOf('failed to find target with hash string') >= 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 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 isAutoGenerated(file) { + return fs.existsSync(file) && fs.readFileSync(file, 'utf8').indexOf(MARKER) > 0; +} diff --git a/StoneIsland/platforms/android/cordova/lib/builders/builders.js b/StoneIsland/platforms/android/cordova/lib/builders/builders.js new file mode 100644 index 00000000..4921c49a --- /dev/null +++ b/StoneIsland/platforms/android/cordova/lib/builders/builders.js @@ -0,0 +1,47 @@ +/* + 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 = { + ant: 'AntBuilder', + gradle: 'GradleBuilder', + 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/StoneIsland/platforms/android/cordova/lib/check_reqs.js b/StoneIsland/platforms/android/cordova/lib/check_reqs.js index 9d251596..ac6fa4c1 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/check_reqs.js +++ b/StoneIsland/platforms/android/cordova/lib/check_reqs.js @@ -26,15 +26,14 @@ var shelljs = require('shelljs'), Q = require('q'), path = require('path'), fs = require('fs'), - which = require('which'), ROOT = path.join(__dirname, '..', '..'); +var CordovaError = require('cordova-common').CordovaError; var isWindows = process.platform == 'win32'; function forgivingWhichSync(cmd) { try { - // TODO: Should use shelljs.which() here to have one less dependency. - return fs.realpathSync(which.sync(cmd)); + return fs.realpathSync(shelljs.which(cmd)); } catch (e) { return ''; } @@ -43,7 +42,7 @@ function forgivingWhichSync(cmd) { function tryCommand(cmd, errMsg, catchStderr) { var d = Q.defer(); child_process.exec(cmd, function(err, stdout, stderr) { - if (err) d.reject(new Error(errMsg)); + 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()); @@ -83,12 +82,12 @@ module.exports.check_ant = function() { module.exports.check_gradle = function() { var sdkDir = process.env['ANDROID_HOME']; if (!sdkDir) - return Q.reject('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.'); + 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 wrapperDir = path.join(sdkDir, 'tools', 'templates', 'gradle', 'wrapper'); if (!fs.existsSync(wrapperDir)) { - return Q.reject(new Error('Could not find gradle wrapper within Android SDK. Might need to update your Android SDK.\n' + + return Q.reject(new CordovaError('Could not find gradle wrapper within Android SDK. Might need to update your Android SDK.\n' + 'Looked here: ' + wrapperDir)); } return Q.when(); @@ -120,7 +119,7 @@ module.exports.check_java = function() { if (fs.existsSync(path.join(maybeJavaHome, 'lib', 'tools.jar'))) { process.env['JAVA_HOME'] = maybeJavaHome; } else { - throw new Error(msg); + throw new CordovaError(msg); } } } else if (isWindows) { @@ -143,22 +142,21 @@ module.exports.check_java = function() { } } }).then(function() { - var msg = - 'Failed to run "java -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'; - } - return tryCommand('java -version', msg) - .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) { - var match = /javac ((?:\d+\.)+(?:\d+))/i.exec(output)[1]; - return match && match[1]; + 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. @@ -212,7 +210,7 @@ module.exports.check_android = function() { process.env['ANDROID_HOME'] = grandParentDir; hasAndroidHome = true; } else { - throw new Error('Failed to find \'ANDROID_HOME\' environment variable. Try setting setting it manually.\n' + + throw new CordovaError('Failed to find \'ANDROID_HOME\' environment variable. Try setting 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 path to valid SDK directory.'); } @@ -221,27 +219,32 @@ module.exports.check_android = function() { process.env['PATH'] += path.delimiter + path.join(process.env['ANDROID_HOME'], 'platform-tools'); } if (!process.env['ANDROID_HOME']) { - throw new Error('Failed to find \'ANDROID_HOME\' environment variable. Try setting setting it manually.\n' + + throw new CordovaError('Failed to find \'ANDROID_HOME\' environment variable. Try setting 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 Error('\'ANDROID_HOME\' environment variable is set to non-existent path: ' + 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.'); } + return hasAndroidHome; }); }; -module.exports.getAbsoluteAndroidCmd = function() { - return forgivingWhichSync('android').replace(/(\s)/g, '\\$1'); +module.exports.getAbsoluteAndroidCmd = function () { + var cmd = forgivingWhichSync('android'); + if (process.platform === 'win32') { + return '"' + cmd + '"'; + } + return cmd.replace(/(\s)/g, '\\$1'); }; -module.exports.check_android_target = function(valid_target) { +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 - if (!valid_target) valid_target = module.exports.get_target(); + var valid_target = module.exports.get_target(); var msg = 'Android SDK not found. Make sure that it is installed. If it is not at the default location, set the ANDROID_HOME environment variable.'; return tryCommand('android list targets --compact', msg) .then(function(output) { @@ -251,24 +254,38 @@ module.exports.check_android_target = function(valid_target) { } var androidCmd = module.exports.getAbsoluteAndroidCmd(); - throw new Error('Please install Android target: "' + valid_target + '".\n\n' + + var msg = 'Please install Android target: "' + valid_target + '".\n\n' + 'Hint: Open the SDK manager by running: ' + androidCmd + '\n' + 'You will require:\n' + '1. "SDK Platform" for ' + valid_target + '\n' + '2. "Android SDK Platform-tools (latest)\n' + - '3. "Android SDK Build-tools" (latest)'); + '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(this.check_android_target)]) - .then(function() { - console.log('ANDROID_HOME=' + process.env['ANDROID_HOME']); - console.log('JAVA_HOME=' + process.env['JAVA_HOME']); - }); + 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. diff --git a/StoneIsland/platforms/android/cordova/lib/device.js b/StoneIsland/platforms/android/cordova/lib/device.js index c13fdc40..4b171db6 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/device.js +++ b/StoneIsland/platforms/android/cordova/lib/device.js @@ -19,40 +19,30 @@ under the License. */ -var exec = require('./exec'), - Q = require('q'), - os = require('os'), - build = require('./build'), - appinfo = require('./appinfo'); +var Q = require('q'), + 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) { - function helper() { - return exec('adb devices', os.tmpdir()) - .then(function(output) { - var response = output.split('\n'); - var device_list = []; - for (var i = 1; i < response.length; i++) { - if (response[i].match(/\w+\tdevice/) && !response[i].match(/emulator/)) { - device_list.push(response[i].replace(/\tdevice/, '').replace('\r', '')); - } - } - return device_list; - }); - } - return helper() + 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 exec('killall adb') + return spawn('killall', ['adb']) .then(function() { - console.log('Restarting adb to see if more devices are detected.'); - return helper(); + events.emit('verbose', 'Restarting adb to see if more devices are detected.'); + return Adb.devices(); }, function() { // For non-killall OS's. return list; @@ -66,7 +56,7 @@ module.exports.resolveTarget = function(target) { return this.list(true) .then(function(device_list) { if (!device_list || !device_list.length) { - return Q.reject('ERROR: Failed to deploy to device, no devices found.'); + return Q.reject(new CordovaError('Failed to deploy to device, no devices found.')); } // default device target = target || device_list[0]; @@ -95,27 +85,36 @@ module.exports.install = function(target, buildResults) { return module.exports.resolveTarget(target); }).then(function(resolvedTarget) { var apk_path = build.findBestApkForArchitecture(buildResults, resolvedTarget.arch); - var launchName = appinfo.getActivityName(); - console.log('Using apk: ' + apk_path); - console.log('Installing app on device...'); - var cmd = 'adb -s ' + resolvedTarget.target + ' install -r "' + apk_path + '"'; - return exec(cmd, os.tmpdir()) - .then(function(output) { - if (output.match(/Failure/)) return Q.reject('ERROR: Failed to install apk to device: ' + output); + var manifest = new AndroidManifest(path.join(__dirname, '../../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); - //unlock screen - var cmd = 'adb -s ' + resolvedTarget.target + ' shell input keyevent 82'; - return exec(cmd, os.tmpdir()); - }, function(err) { return Q.reject('ERROR: Failed to install apk to device: ' + err); }) + 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() { - // launch the application - console.log('Launching application...'); - var cmd = 'adb -s ' + resolvedTarget.target + ' shell am start -W -a android.intent.action.MAIN -n ' + launchName; - return exec(cmd, os.tmpdir()); + //unlock screen + return Adb.shell(resolvedTarget.target, 'input keyevent 82'); + }).then(function() { + return Adb.start(resolvedTarget.target, launchName); }).then(function() { - console.log('LAUNCH SUCCESS'); - }, function(err) { - return Q.reject('ERROR: Failed to launch application on device: ' + err); + events.emit('log', 'LAUNCH SUCCESS'); }); }); }; diff --git a/StoneIsland/platforms/android/cordova/lib/emulator.js b/StoneIsland/platforms/android/cordova/lib/emulator.js index e81dd679..ff1e261c 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/emulator.js +++ b/StoneIsland/platforms/android/cordova/lib/emulator.js @@ -21,11 +21,14 @@ /* jshint sub:true */ -var exec = require('./exec'); -var appinfo = require('./appinfo'); var retry = require('./retry'); var build = require('./build'); -var check_reqs = require('./check_reqs'); +var path = require('path'); +var Adb = require('./Adb'); +var AndroidManifest = require('./AndroidManifest'); +var events = require('cordova-common').events; +var spawn = require('cordova-common').superspawn.spawn; +var CordovaError = require('cordova-common').CordovaError; var Q = require('q'); var os = require('os'); @@ -33,8 +36,10 @@ var child_process = require('child_process'); // constants var ONE_SECOND = 1000; // in milliseconds -var INSTALL_COMMAND_TIMEOUT = 120 * ONE_SECOND; // 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'; /** @@ -48,7 +53,7 @@ var EXEC_KILL_SIGNAL = 'SIGKILL'; } */ module.exports.list_images = function() { - return exec('android list avds') + return spawn('android', ['list', 'avds']) .then(function(output) { var response = output.split('\n'); var emulator_list = []; @@ -57,13 +62,18 @@ module.exports.list_images = function() { 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/)) { + if (response[i + 1].match(/\(API\slevel\s/) || (response[i + 2] && response[i + 2].match(/\(API\slevel\s/))) { i++; - img_obj['target'] = response[i].replace('\r', ''); + 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++; @@ -92,11 +102,15 @@ module.exports.list_images = function() { * Returns a promise. */ module.exports.best_image = function() { - var project_target = check_reqs.get_target().replace('android-', ''); 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]; + // Loading check_reqs at run-time to avoid test-time vs run-time directory structure difference issue + var project_target = require('./check_reqs').get_target().replace('android-', ''); for (var i in images) { var target = images[i].target; if(target) { @@ -115,22 +129,12 @@ module.exports.best_image = function() { // Returns a promise. module.exports.list_started = function() { - return exec('adb devices', os.tmpdir()) - .then(function(output) { - var response = output.split('\n'); - var started_emulator_list = []; - for (var i = 1; i < response.length; i++) { - if (response[i].match(/device/) && response[i].match(/emulator/)) { - started_emulator_list.push(response[i].replace(/\tdevice/, '').replace('\r', '')); - } - } - return started_emulator_list; - }); + return Adb.devices({emulators: true}); }; // Returns a promise. module.exports.list_targets = function() { - return exec('android list targets', os.tmpdir()) + return spawn('android', ['list', 'targets'], {cwd: os.tmpdir()}) .then(function(output) { var target_out = output.split('\n'); var targets = []; @@ -144,108 +148,139 @@ module.exports.list_targets = function() { }; /* + * 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 used the first image available, + * 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) { +module.exports.start = function(emulator_ID, boot_timeout) { var self = this; - var emulator_id, num_started, started_emulators; - return self.list_started() - .then(function(list) { - started_emulators = list; - num_started = started_emulators.length; - if (!emulator_ID) { - return self.list_images() - .then(function(emulator_list) { - if (emulator_list.length > 0) { - return self.best_image() - .then(function(best) { - emulator_ID = best.name; - console.log('WARNING : no emulator specified, defaulting to ' + emulator_ID); - return emulator_ID; - }); - } else { - var androidCmd = check_reqs.getAbsoluteAndroidCmd(); - return Q.reject('ERROR : 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'); - } - }); - } else { - return Q(emulator_ID); - } - }).then(function() { - var cmd = 'emulator'; - var args = ['-avd', emulator_ID]; - var proc = child_process.spawn(cmd, args, { stdio: 'inherit', detached: true }); - proc.unref(); // Don't wait for it to finish, since the emulator will probably keep running for a long time. - }).then(function() { - // wait for emulator to start - console.log('Waiting for emulator...'); - return self.wait_for_emulator(num_started); - }).then(function(new_started) { - if (new_started.length > 1) { - for (var i in new_started) { - if (started_emulators.indexOf(new_started[i]) < 0) { - emulator_id = new_started[i]; - } + 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; } - } else { - emulator_id = new_started[0]; - } - if (!emulator_id) return Q.reject('ERROR : Failed to start emulator, could not find new emulator'); - //wait for emulator to boot up - process.stdout.write('Booting up emulator (this may take a while)...'); - return self.wait_for_boot(emulator_id); - }).then(function() { - console.log('BOOT COMPLETE'); + // Loading check_reqs at run-time to avoid test-time vs run-time directory structure difference issue + var androidCmd = require('./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) { + 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 }) + .unref(); - //unlock screen - return exec('adb -s ' + emulator_id + ' shell input keyevent 82', os.tmpdir()); - }).then(function() { - //return the new emulator id for the started emulators - return emulator_id; + // 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 the new emulator to apear on the started-emulator list. - * Returns a promise with a list of newly started emulators' IDs. + * Waits for an emulator to boot on a given port. + * Returns this emulator's ID in a promise. */ -module.exports.wait_for_emulator = function(num_running) { +module.exports.wait_for_emulator = function(port) { var self = this; - return self.list_started() - .then(function(new_started) { - if (new_started.length > num_running) { - return new_started; - } else { - return Q.delay(1000).then(function() { - return self.wait_for_emulator(num_running); - }); - } - }); + 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 boot animation property of the emulator to switch to 'stopped' + * 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) { +module.exports.wait_for_boot = function(emulator_id, time_remaining) { var self = this; - return exec('adb -s ' + emulator_id + ' shell getprop init.svc.bootanim', os.tmpdir()) + return Adb.shell(emulator_id, 'ps') .then(function(output) { - if (output.match(/stopped/)) { - return; + if (output.match(/android\.process\.acore/)) { + return true; + } else if (time_remaining === 0) { + return false; } else { process.stdout.write('.'); - return Q.delay(3000).then(function() { - return self.wait_for_boot(emulator_id); + + // 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); }); } }); @@ -257,9 +292,9 @@ module.exports.wait_for_boot = function(emulator_id) { * Returns a promise. */ module.exports.create_image = function(name, target) { - console.log('Creating avd named ' + name); + console.log('Creating new avd named ' + name); if (target) { - return exec('android create avd --name ' + name + ' --target ' + target) + return 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 + '?'); @@ -267,11 +302,11 @@ module.exports.create_image = function(name, target) { }); } else { console.log('WARNING : Project target not found, creating avd with a different target but the project may fail to install.'); - return exec('android create avd --name ' + name + ' --target ' + this.list_targets()[0]) + return 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('Please insure you have targets available by running the "android" command'); + 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 : '); @@ -284,7 +319,7 @@ module.exports.resolveTarget = function(target) { return this.list_started() .then(function(emulator_list) { if (emulator_list.length < 1) { - return Q.reject('No started emulators found, please start an emultor before deploying your project.'); + return Q.reject('No running Android emulators found, please start an emulator before deploying your project.'); } // default emulator @@ -309,6 +344,8 @@ module.exports.resolveTarget = function(target) { module.exports.install = function(givenTarget, buildResults) { var target; + var manifest = new AndroidManifest(path.join(__dirname, '../../AndroidManifest.xml')); + var pkgName = manifest.getPackageId(); // resolve the target emulator return Q().then(function () { @@ -324,49 +361,83 @@ module.exports.install = function(givenTarget, buildResults) { // 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 = { - timeout: INSTALL_COMMAND_TIMEOUT, // in milliseconds - killSignal: EXEC_KILL_SIGNAL - }; + var apk_path = build.findBestApkForArchitecture(buildResults, target.arch); + var execOptions = { + cwd: os.tmpdir(), + timeout: INSTALL_COMMAND_TIMEOUT, // in milliseconds + killSignal: EXEC_KILL_SIGNAL + }; - console.log('Installing app on emulator...'); - console.log('Using apk: ' + apk_path); + events.emit('log', 'Using apk: ' + apk_path); + events.emit('log', 'Package name: ' + pkgName); + events.emit('verbose', 'Installing app on emulator...'); - var retriedInstall = retry.retryPromise( - NUM_INSTALL_RETRIES, - exec, 'adb -s ' + target.target + ' install -r -d "' + apk_path + '"', os.tmpdir(), execOptions - ); + // 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 + '...'); - return retriedInstall.then(function (output) { - if (output.match(/Failure/)) { - return Q.reject('Failed to install apk to emulator: ' + output); - } else { - console.log('INSTALL SUCCESS'); + 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 (err) { - return Q.reject('Failed to install apk to emulator: ' + err); - }); - // unlock screen - }).then(function () { + 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; - console.log('Unlocking screen...'); - return exec('adb -s ' + target.target + ' shell input keyevent 82', os.tmpdir()); + events.emit('warn', 'Uninstalling app from device and reinstalling it because the ' + + 'currently installed app was signed with different key'); - // launch the application - }).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 Adb.uninstall(target.target, pkgName) + .then(function() { + return adbInstallWithOptions(target.target, apk_path, execOptions); + }); + }); + } - console.log('Launching application...'); - var launchName = appinfo.getActivityName(); - var cmd = 'adb -s ' + target.target + ' shell am start -W -a android.intent.action.MAIN -n ' + launchName; - return exec(cmd, os.tmpdir()); + 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) { - console.log('LAUNCH SUCCESS'); - }, function (err) { - return Q.reject('Failed to launch app on emulator: ' + err); + events.emit('log', 'LAUNCH SUCCESS'); }); }; diff --git a/StoneIsland/platforms/android/cordova/lib/exec.js b/StoneIsland/platforms/android/cordova/lib/exec.js deleted file mode 100755 index 798a93ba..00000000 --- a/StoneIsland/platforms/android/cordova/lib/exec.js +++ /dev/null @@ -1,68 +0,0 @@ -#!/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 child_process = require("child_process"); -var Q = require("q"); - -// constants -var DEFAULT_MAX_BUFFER = 1024000; - -// Takes a command and optional current working directory. -// Returns a promise that either resolves with the stdout, or -// rejects with an error message and the stderr. -// -// WARNING: -// opt_cwd is an artifact of an old design, and must -// be removed in the future; the correct solution is -// to pass the options object the same way that -// child_process.exec expects -// -// NOTE: -// exec documented here - https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback -module.exports = function(cmd, opt_cwd, options) { - - var d = Q.defer(); - - if (typeof options === "undefined") { - options = {}; - } - - // override cwd to preserve old opt_cwd behavior - options.cwd = opt_cwd; - - // set maxBuffer - if (typeof options.maxBuffer === "undefined") { - options.maxBuffer = DEFAULT_MAX_BUFFER; - } - - try { - child_process.exec(cmd, options, function(err, stdout, stderr) { - if (err) d.reject("Error executing \"" + cmd + "\": " + stderr); - else d.resolve(stdout); - }); - } catch(e) { - console.error("error caught: " + e); - d.reject(e); - } - - return d.promise; -}; - diff --git a/StoneIsland/platforms/android/cordova/lib/install-device.bat b/StoneIsland/platforms/android/cordova/lib/install-device.bat index ac7214ac..ac7214ac 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/install-device.bat +++ b/StoneIsland/platforms/android/cordova/lib/install-device.bat diff --git a/StoneIsland/platforms/android/cordova/lib/install-emulator.bat b/StoneIsland/platforms/android/cordova/lib/install-emulator.bat index 1ec67790..1ec67790 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/install-emulator.bat +++ b/StoneIsland/platforms/android/cordova/lib/install-emulator.bat diff --git a/StoneIsland/platforms/android/cordova/lib/list-devices b/StoneIsland/platforms/android/cordova/lib/list-devices index e390bff6..fa84d7f6 100755 --- a/StoneIsland/platforms/android/cordova/lib/list-devices +++ b/StoneIsland/platforms/android/cordova/lib/list-devices @@ -22,12 +22,13 @@ var devices = require('./device'); // Usage support for when args are given -devices.list().done(function(device_list) { - device_list && device_list.forEach(function(dev) { - console.log(dev); +require('../lib/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); }); -}, function(err) { - console.error('ERROR: ' + err); - process.exit(2); }); - diff --git a/StoneIsland/platforms/android/cordova/lib/list-devices.bat b/StoneIsland/platforms/android/cordova/lib/list-devices.bat index c0bcdd9a..c0bcdd9a 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/list-devices.bat +++ b/StoneIsland/platforms/android/cordova/lib/list-devices.bat diff --git a/StoneIsland/platforms/android/cordova/lib/list-emulator-images b/StoneIsland/platforms/android/cordova/lib/list-emulator-images index 996cf555..03c827fe 100755 --- a/StoneIsland/platforms/android/cordova/lib/list-emulator-images +++ b/StoneIsland/platforms/android/cordova/lib/list-emulator-images @@ -22,11 +22,13 @@ var emulators = require('./emulator'); // Usage support for when args are given -emulators.list_images().done(function(emulator_list) { - emulator_list && emulator_list.forEach(function(emu) { - console.log(emu.name); +require('../lib/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); }); -}, function(err) { - console.error('ERROR: ' + err); - process.exit(2); }); diff --git a/StoneIsland/platforms/android/cordova/lib/list-emulator-images.bat b/StoneIsland/platforms/android/cordova/lib/list-emulator-images.bat index 661cbf95..661cbf95 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/list-emulator-images.bat +++ b/StoneIsland/platforms/android/cordova/lib/list-emulator-images.bat diff --git a/StoneIsland/platforms/android/cordova/lib/list-started-emulators b/StoneIsland/platforms/android/cordova/lib/list-started-emulators index 2ae8c5a8..a890dec6 100755 --- a/StoneIsland/platforms/android/cordova/lib/list-started-emulators +++ b/StoneIsland/platforms/android/cordova/lib/list-started-emulators @@ -22,11 +22,13 @@ var emulators = require('./emulator'); // Usage support for when args are given -emulators.list_started().done(function(emulator_list) { - emulator_list && emulator_list.forEach(function(emu) { - console.log(emu); +require('../lib/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); }); -}, function(err) { - console.error('ERROR: ' + err); - process.exit(2); }); diff --git a/StoneIsland/platforms/android/cordova/lib/list-started-emulators.bat b/StoneIsland/platforms/android/cordova/lib/list-started-emulators.bat index a4e88f7d..a4e88f7d 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/list-started-emulators.bat +++ b/StoneIsland/platforms/android/cordova/lib/list-started-emulators.bat diff --git a/StoneIsland/platforms/android/cordova/lib/log.js b/StoneIsland/platforms/android/cordova/lib/log.js index ebf836d5..ebf836d5 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/log.js +++ b/StoneIsland/platforms/android/cordova/lib/log.js diff --git a/StoneIsland/platforms/android/cordova/lib/plugin-build.gradle b/StoneIsland/platforms/android/cordova/lib/plugin-build.gradle index b345b90a..d1c63365 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/plugin-build.gradle +++ b/StoneIsland/platforms/android/cordova/lib/plugin-build.gradle @@ -21,28 +21,19 @@ buildscript { repositories { mavenCentral() + jcenter() } // Switch the Android Gradle plugin version requirement depending on the // installed version of Gradle. This dependency is documented at // http://tools.android.com/tech-docs/new-build-system/version-compatibility // and https://issues.apache.org/jira/browse/CB-8143 - if (gradle.gradleVersion >= "2.2") { - dependencies { - classpath 'com.android.tools.build:gradle:1.0.0+' - } - } else if (gradle.gradleVersion >= "2.1") { - dependencies { - classpath 'com.android.tools.build:gradle:0.14.0+' - } - } else { - dependencies { - classpath 'com.android.tools.build:gradle:0.12.0+' - } + dependencies { + classpath 'com.android.tools.build:gradle:1.0.0+' } } -apply plugin: 'android-library' +apply plugin: 'com.android.library' dependencies { compile fileTree(dir: 'libs', include: '*.jar') diff --git a/StoneIsland/platforms/android/cordova/lib/pluginHandlers.js b/StoneIsland/platforms/android/cordova/lib/pluginHandlers.js new file mode 100644 index 00000000..5e745fd5 --- /dev/null +++ b/StoneIsland/platforms/android/cordova/lib/pluginHandlers.js @@ -0,0 +1,308 @@ +/* + * + * Copyright 2013 Anis Kadri + * + * 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)); + + if(options && options.android_studio === true) { + dest = path.join('app/src/main/java', obj.targetDir.substring(4), 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) { + copyFile(plugin.dir, obj.src, project.projectDir, path.normalize(obj.target), !!(options && options.link)); + }, + uninstall:function(obj, plugin, project, options) { + removeFile(project.projectDir, path.normalize(obj.target)); + } + }, + '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/StoneIsland/platforms/android/cordova/lib/prepare.js b/StoneIsland/platforms/android/cordova/lib/prepare.js new file mode 100644 index 00000000..10a69ea3 --- /dev/null +++ b/StoneIsland/platforms/android/cordova/lib/prepare.js @@ -0,0 +1,431 @@ +/** + 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 path = require('path'); +var shell = require('shelljs'); +var events = require('cordova-common').events; +var AndroidManifest = require('./AndroidManifest'); +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)); + }) + .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)); + }); +}; + +/** + * 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 name = platformConfig.name(); + var strings = xmlHelpers.parseElementtreeSync(locations.strings); + strings.find('string[@name="app_name"]').text = name.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 pkg = (platformConfig.android_packageName() || platformConfig.packageName()).replace(/-/g, '_'); + + var manifest = new AndroidManifest(locations.manifest); + var orig_pkg = manifest.getPackageId(); + + manifest.getActivity() + .setOrientation(platformConfig.getPreference('orientation')) + .setLaunchMode(findAndroidLaunchModePreference(platformConfig)); + + manifest.setVersionName(platformConfig.version()) + .setVersionCode(platformConfig.android_versionCode() || default_versionCode(platformConfig.version())) + .setPackageId(pkg) + .setMinSdkVersion(platformConfig.getPreference('android-minSdkVersion', 'android')) + .setMaxSdkVersion(platformConfig.getPreference('android-maxSdkVersion', 'android')) + .setTargetSdkVersion(platformConfig.getPreference('android-targetSdkVersion', 'android')) + .write(); + + var javaPattern = path.join(locations.root, 'src', orig_pkg.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, 'src', pkg.replace(/\./g, '/'), path.basename(java_files[0])); + shell.mkdir('-p', path.dirname(destFile)); + shell.sed(/package [\w\.]*;/, 'package ' + pkg + ';', java_files[0]).to(destFile); + events.emit('verbose', 'Wrote out Android package name "' + pkg + '" to ' + destFile); + + if (orig_pkg !== pkg) { + // 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; +} + +/** + * 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/StoneIsland/platforms/android/cordova/lib/retry.js b/StoneIsland/platforms/android/cordova/lib/retry.js index dc52a7d2..3cb49274 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/retry.js +++ b/StoneIsland/platforms/android/cordova/lib/retry.js @@ -21,7 +21,9 @@ /* jshint node: true */ -"use strict"; +'use strict'; + +var events = require('cordova-common').events; /* * Retry a promise-returning function a number of times, propagating its @@ -56,7 +58,7 @@ module.exports.retryPromise = function (attemts_left, promiseFunction) { throw error; } - console.log("A retried call failed. Retrying " + attemts_left + " more time(s)."); + 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); diff --git a/StoneIsland/platforms/android/cordova/lib/run.js b/StoneIsland/platforms/android/cordova/lib/run.js index 7f15448c..214a1e19 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/run.js +++ b/StoneIsland/platforms/android/cordova/lib/run.js @@ -25,63 +25,36 @@ var path = require('path'), build = require('./build'), emulator = require('./emulator'), device = require('./device'), - shell = require('shelljs'), - Q = require('q'); + Q = require('q'), + events = require('cordova-common').events; -/* - * 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. - * Returns a promise. - */ - module.exports.run = function(args) { - var buildFlags = []; +function getInstallTarget(runOptions) { var install_target; - var list = false; - - for (var i=2; i<args.length; i++) { - if (build.isBuildFlag(args[i])) { - buildFlags.push(args[i]); - } else if (args[i] == '--device') { - install_target = '--device'; - } else if (args[i] == '--emulator') { - install_target = '--emulator'; - } else if (/^--target=/.exec(args[i])) { - install_target = args[i].substring(9, args[i].length); - } else if (args[i] == '--list') { - list = true; - } else { - console.warn('Option \'' + args[i] + '\' not recognized (ignoring).'); - } + if (runOptions.target) { + install_target = runOptions.target; + } else if (runOptions.device) { + install_target = '--device'; + } else if (runOptions.emulator) { + install_target = '--emulator'; } - if (list) { - var output = ''; - var temp = ''; - if (!install_target) { - output += 'Available Android Devices:\n'; - temp = shell.exec(path.join(__dirname, 'list-devices'), {silent:true}).output; - temp = temp.replace(/^(?=[^\s])/gm, '\t'); - output += temp; - output += 'Available Android Virtual Devices:\n'; - temp = shell.exec(path.join(__dirname, 'list-emulator-images'), {silent:true}).output; - temp = temp.replace(/^(?=[^\s])/gm, '\t'); - output += temp; - } else if (install_target == '--emulator') { - output += 'Available Android Virtual Devices:\n'; - temp = shell.exec(path.join(__dirname, 'list-emulator-images'), {silent:true}).output; - temp = temp.replace(/^(?=[^\s])/gm, '\t'); - output += temp; - } else if (install_target == '--device') { - output += 'Available Android Devices:\n'; - temp = shell.exec(path.join(__dirname, 'list-devices'), {silent:true}).output; - temp = temp.replace(/^(?=[^\s])/gm, '\t'); - output += temp; - } - console.log(output); - return; - } + return 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() { @@ -90,10 +63,10 @@ var path = require('path'), return device.list() .then(function(device_list) { if (device_list.length > 0) { - console.log('WARNING : No target specified, deploying to device \'' + device_list[0] + '\'.'); + events.emit('warn', 'No target specified, deploying to device \'' + device_list[0] + '\'.'); install_target = device_list[0]; } else { - console.log('WARNING : No target specified, deploying to emulator'); + events.emit('warn', 'No target specified and no devices found, deploying to emulator'); install_target = '--emulator'; } }); @@ -137,17 +110,25 @@ var path = require('path'), }); }); }).then(function(resolvedTarget) { - return build.run(buildFlags, resolvedTarget).then(function(buildResults) { + // 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.install(resolvedTarget, buildResults); + return emulator.wait_for_boot(resolvedTarget.target) + .then(function () { + return emulator.install(resolvedTarget, buildResults); + }); } return device.install(resolvedTarget, buildResults); }); }); }; -module.exports.help = function(args) { - console.log('Usage: ' + path.relative(process.cwd(), args[1]) + ' [options]'); +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'); diff --git a/StoneIsland/platforms/android/cordova/lib/spawn.js b/StoneIsland/platforms/android/cordova/lib/spawn.js deleted file mode 100755 index 3e500a09..00000000 --- a/StoneIsland/platforms/android/cordova/lib/spawn.js +++ /dev/null @@ -1,50 +0,0 @@ -#!/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 child_process = require('child_process'), - Q = require('q'); -var isWindows = process.platform.slice(0, 3) == 'win'; - -// Takes a command and optional current working directory. -module.exports = function(cmd, args, opt_cwd) { - var d = Q.defer(); - var opts = { cwd: opt_cwd, stdio: 'inherit' }; - try { - // Work around spawn not being able to find .bat files. - if (isWindows) { - args = [['/s', '/c', '"' + [cmd].concat(args).map(function(a){if (/^[^"].* .*[^"]/.test(a)) return '"' + a + '"'; return a;}).join(' ')+'"'].join(' ')]; - cmd = 'cmd'; - opts.windowsVerbatimArguments = true; - } - var child = child_process.spawn(cmd, args, opts); - child.on('exit', function(code) { - if (code) { - d.reject('Error code ' + code + ' for command: ' + cmd + ' with args: ' + args); - } else { - d.resolve(); - } - }); - } catch(e) { - console.error('error caught: ' + e); - d.reject(e); - } - return d.promise; -}; diff --git a/StoneIsland/platforms/android/cordova/lib/start-emulator.bat b/StoneIsland/platforms/android/cordova/lib/start-emulator.bat index 9329d951..9329d951 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/start-emulator.bat +++ b/StoneIsland/platforms/android/cordova/lib/start-emulator.bat diff --git a/StoneIsland/platforms/android/cordova/log.bat b/StoneIsland/platforms/android/cordova/log.bat index 4b2b434e..4b2b434e 100755..100644 --- a/StoneIsland/platforms/android/cordova/log.bat +++ b/StoneIsland/platforms/android/cordova/log.bat diff --git a/StoneIsland/platforms/android/cordova/loggingHelper.js b/StoneIsland/platforms/android/cordova/loggingHelper.js new file mode 100644 index 00000000..32b2ee0a --- /dev/null +++ b/StoneIsland/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/StoneIsland/platforms/android/cordova/run b/StoneIsland/platforms/android/cordova/run index 8c6fe38c..9544c1dc 100755 --- a/StoneIsland/platforms/android/cordova/run +++ b/StoneIsland/platforms/android/cordova/run @@ -19,19 +19,35 @@ under the License. */ -var run = require('./lib/run'), - reqs = require('./lib/check_reqs'), - args = process.argv; +var Api = require('./Api'); +var nopt = require('nopt'); +var path = require('path'); // Support basic help commands -if (args[2] == '--help' || args[2] == '/?' || args[2] == '-h' || - args[2] == 'help' || args[2] == '-help' || args[2] == '/help') { - run.help(args); -} else { - reqs.run().done(function() { - return run.run(args); - }, function(err) { - console.error('ERROR: ' + err); - process.exit(2); - }); -} +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/StoneIsland/platforms/android/cordova/run.bat b/StoneIsland/platforms/android/cordova/run.bat index b0bc28b2..b0bc28b2 100755..100644 --- a/StoneIsland/platforms/android/cordova/run.bat +++ b/StoneIsland/platforms/android/cordova/run.bat diff --git a/StoneIsland/platforms/android/cordova/version b/StoneIsland/platforms/android/cordova/version index 07442655..9bf79a18 100755 --- a/StoneIsland/platforms/android/cordova/version +++ b/StoneIsland/platforms/android/cordova/version @@ -20,6 +20,10 @@ */ // Coho updates this line: -var VERSION = "4.1.1"; +var VERSION = "6.1.2"; -console.log(VERSION); +module.exports.version = VERSION; + +if (!module.parent) { + console.log(VERSION); +} diff --git a/StoneIsland/platforms/android/cordova/version.bat b/StoneIsland/platforms/android/cordova/version.bat index 3610c17b..3610c17b 100755..100644 --- a/StoneIsland/platforms/android/cordova/version.bat +++ b/StoneIsland/platforms/android/cordova/version.bat diff --git a/StoneIsland/platforms/android/google-services.json b/StoneIsland/platforms/android/google-services.json new file mode 100644 index 00000000..57497bf5 --- /dev/null +++ b/StoneIsland/platforms/android/google-services.json @@ -0,0 +1,4 @@ +{ + "project_info": {}, + "client": [] +} diff --git a/StoneIsland/platforms/android/phonegap-plugin-push/stoneisland-push.gradle b/StoneIsland/platforms/android/phonegap-plugin-push/stoneisland-push.gradle new file mode 100644 index 00000000..11e735ae --- /dev/null +++ b/StoneIsland/platforms/android/phonegap-plugin-push/stoneisland-push.gradle @@ -0,0 +1,21 @@ +import java.util.regex.Pattern + +def doExtractStringFromManifest(name) { + def manifestFile = file(android.sourceSets.main.manifest.srcFile) + def pattern = Pattern.compile(name + "=\"(.*?)\"") + def matcher = pattern.matcher(manifestFile.getText()) + matcher.find() + return matcher.group(1) +} + +android { + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + } + } + + defaultConfig { + applicationId = doExtractStringFromManifest("package") + } +} diff --git a/StoneIsland/platforms/android/platform_www/cordova-js-src/android/nativeapiprovider.js b/StoneIsland/platforms/android/platform_www/cordova-js-src/android/nativeapiprovider.js index 2e9aa67b..2e9aa67b 100755..100644 --- a/StoneIsland/platforms/android/platform_www/cordova-js-src/android/nativeapiprovider.js +++ b/StoneIsland/platforms/android/platform_www/cordova-js-src/android/nativeapiprovider.js diff --git a/StoneIsland/platforms/android/platform_www/cordova-js-src/android/promptbasednativeapi.js b/StoneIsland/platforms/android/platform_www/cordova-js-src/android/promptbasednativeapi.js index f7fb6bc7..f7fb6bc7 100755..100644 --- a/StoneIsland/platforms/android/platform_www/cordova-js-src/android/promptbasednativeapi.js +++ b/StoneIsland/platforms/android/platform_www/cordova-js-src/android/promptbasednativeapi.js diff --git a/StoneIsland/platforms/android/platform_www/cordova-js-src/exec.js b/StoneIsland/platforms/android/platform_www/cordova-js-src/exec.js index fa8b41be..f73d87a1 100755..100644 --- a/StoneIsland/platforms/android/platform_www/cordova-js-src/exec.js +++ b/StoneIsland/platforms/android/platform_www/cordova-js-src/exec.js @@ -51,10 +51,11 @@ var cordova = require('cordova'), // 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 + ONLINE_EVENT: 2, + EVAL_BRIDGE: 3 }, jsToNativeBridgeMode, // Set lazily. - nativeToJsBridgeMode = nativeToJsModes.ONLINE_EVENT, + nativeToJsBridgeMode = nativeToJsModes.EVAL_BRIDGE, pollEnabled = false, bridgeSecret = -1; @@ -77,6 +78,9 @@ function androidExec(success, fail, service, action, args) { 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') { @@ -86,7 +90,6 @@ function androidExec(success, fail, service, action, args) { var callbackId = service + cordova.callbackId++, argsJson = JSON.stringify(args); - if (success || fail) { cordova.callbacks[callbackId] = {success:success, fail:fail}; } @@ -106,6 +109,17 @@ function androidExec(success, fail, service, action, args) { } 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(); }; diff --git a/StoneIsland/platforms/android/platform_www/cordova-js-src/platform.js b/StoneIsland/platforms/android/platform_www/cordova-js-src/platform.js index bffc6751..2bfd0247 100755..100644 --- a/StoneIsland/platforms/android/platform_www/cordova-js-src/platform.js +++ b/StoneIsland/platforms/android/platform_www/cordova-js-src/platform.js @@ -19,6 +19,9 @@ * */ +// The last resume event that was received that had the result of a plugin call. +var lastResumeEvent = null; + module.exports = { id: 'android', bootstrap: function() { @@ -58,6 +61,19 @@ module.exports = { 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() { @@ -79,12 +95,30 @@ function onMessageFromNative(msg) { case 'searchbutton': // App life cycle events case 'pause': - case 'resume': // 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/StoneIsland/platforms/android/platform_www/cordova-js-src/plugin/android/app.js b/StoneIsland/platforms/android/platform_www/cordova-js-src/plugin/android/app.js index 22cf96e8..22cf96e8 100755..100644 --- a/StoneIsland/platforms/android/platform_www/cordova-js-src/plugin/android/app.js +++ b/StoneIsland/platforms/android/platform_www/cordova-js-src/plugin/android/app.js diff --git a/StoneIsland/platforms/android/platform_www/cordova.js b/StoneIsland/platforms/android/platform_www/cordova.js index 23f6e475..18c020e7 100755..100644 --- a/StoneIsland/platforms/android/platform_www/cordova.js +++ b/StoneIsland/platforms/android/platform_www/cordova.js @@ -1,5 +1,5 @@ // Platform: android -// 2c29e187e4206a6a77fba940ef6f77aef5c7eb8c +// 7c5fcc5a5adfbf3fb8ceaf36fbdd4bd970bd9c20 /* Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file @@ -19,7 +19,7 @@ under the License. */ ;(function() { -var PLATFORM_VERSION_BUILD_LABEL = '4.1.1'; +var PLATFORM_VERSION_BUILD_LABEL = '6.1.2'; // file: src/scripts/require.js /*jshint -W079 */ @@ -101,7 +101,9 @@ if (typeof module === "object" && typeof require === "function") { // file: src/cordova.js define("cordova", function(require, exports, module) { -if(window.cordova){ +// 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)) { throw new Error("cordova already defined"); } @@ -740,8 +742,13 @@ var Channel = function(type, sticky) { } }; -function forceFunction(f) { - if (typeof f != 'function') throw "Function required as first argument!"; +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." + ); + } } /** @@ -751,28 +758,39 @@ function forceFunction(f) { * and a guid that can be used to stop subscribing to the channel. * Returns the guid. */ -Channel.prototype.subscribe = function(f, c) { - // need a function to call - forceFunction(f); +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) { - f.apply(c || this, this.fireArgs); + handleEvent.apply(eventListener || this, this.fireArgs); return; } - var func = f, - guid = f.observer_guid; - if (typeof c == "object") { func = utils.close(c, f); } + guid = eventListenerOrFunction.observer_guid; + if (typeof eventListener === "object") { + handleEvent = utils.close(eventListener, handleEvent); + } if (!guid) { - // first time any channel has seen this subscriber + // First time any channel has seen this subscriber guid = '' + nextGuid++; } - func.observer_guid = guid; - f.observer_guid = guid; + handleEvent.observer_guid = guid; + eventListenerOrFunction.observer_guid = guid; // Don't add the same handler more than once. if (!this.handlers[guid]) { - this.handlers[guid] = func; + this.handlers[guid] = handleEvent; this.numHandlers++; if (this.numHandlers == 1) { this.onHasSubscribersChange && this.onHasSubscribersChange(); @@ -783,12 +801,20 @@ Channel.prototype.subscribe = function(f, c) { /** * Unsubscribes the function with the given guid from the channel. */ -Channel.prototype.unsubscribe = function(f) { - // need a function to unsubscribe - forceFunction(f); +Channel.prototype.unsubscribe = function(eventListenerOrFunction) { + checkSubscriptionArgument(eventListenerOrFunction); + var handleEvent, guid, handler; - var guid = f.observer_guid, - handler = this.handlers[guid]; + 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--; @@ -895,10 +921,11 @@ var cordova = require('cordova'), // 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 + ONLINE_EVENT: 2, + EVAL_BRIDGE: 3 }, jsToNativeBridgeMode, // Set lazily. - nativeToJsBridgeMode = nativeToJsModes.ONLINE_EVENT, + nativeToJsBridgeMode = nativeToJsModes.EVAL_BRIDGE, pollEnabled = false, bridgeSecret = -1; @@ -921,6 +948,9 @@ function androidExec(success, fail, service, action, args) { 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') { @@ -930,7 +960,6 @@ function androidExec(success, fail, service, action, args) { var callbackId = service + cordova.callbackId++, argsJson = JSON.stringify(args); - if (success || fail) { cordova.callbacks[callbackId] = {success:success, fail:fail}; } @@ -950,6 +979,17 @@ function androidExec(success, fail, service, action, args) { } 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(); }; @@ -1291,10 +1331,12 @@ 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]; +var platformInitChannelsArray = [channel.onDOMContentLoaded, channel.onNativeReady, channel.onPluginsReady]; // setting exec cordova.exec = require('cordova/exec'); @@ -1379,10 +1421,19 @@ if (window._nativeReady) { // 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(); @@ -1501,9 +1552,109 @@ exports.reset(); }); +// file: src/common/modulemapper_b.js +define("cordova/modulemapper_b", function(require, exports, module) { + +var builder = require('cordova/builder'), + symbolList = [], + 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) { + 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); + // <runs/> + 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() { @@ -1543,6 +1694,19 @@ module.exports = { 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() { @@ -1564,12 +1728,30 @@ function onMessageFromNative(msg) { case 'searchbutton': // App life cycle events case 'pause': - case 'resume': // 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); } @@ -1673,10 +1855,6 @@ module.exports = { // file: src/common/pluginloader.js define("cordova/pluginloader", function(require, exports, module) { -/* - NOTE: this file is NOT used when we use the browserify workflow -*/ - var modulemapper = require('cordova/modulemapper'); var urlutil = require('cordova/urlutil'); @@ -1786,6 +1964,54 @@ exports.load = function(callback) { }); +// file: src/common/pluginloader_b.js +define("cordova/pluginloader_b", function(require, exports, module) { + +var modulemapper = require('cordova/modulemapper'); + +// Handler for the cordova_plugins.js content. +// See plugman's plugin_loader.js for the details of this object. +function handlePluginsObject(moduleList) { + // if moduleList is not defined or empty, we've nothing to do + if (!moduleList || !moduleList.length) { + return; + } + + // Loop through all the modules and then through their clobbers and merges. + for (var i = 0, module; module = moduleList[i]; i++) { + if (module.clobbers && module.clobbers.length) { + for (var j = 0; j < module.clobbers.length; j++) { + modulemapper.clobbers(module.id, module.clobbers[j]); + } + } + + if (module.merges && module.merges.length) { + for (var k = 0; k < module.merges.length; k++) { + modulemapper.merges(module.id, module.merges[k]); + } + } + + // Finally, if runs is truthy we want to simply require() the module. + if (module.runs) { + modulemapper.runs(module.id); + } + } +} + +// Loads all plugins' js-modules. Plugin loading is syncronous in browserified bundle +// but the method accepts callback to be compatible with non-browserify flow. +// onDeviceReady is blocked on onPluginsReady. onPluginsReady is fired when there are +// no plugins to load, or they are all done. +exports.load = function(callback) { + var moduleList = require("cordova/plugin_list"); + handlePluginsObject(moduleList); + + callback(); +}; + + +}); + // file: src/common/urlutil.js define("cordova/urlutil", function(require, exports, module) { @@ -1895,7 +2121,10 @@ utils.clone = function(obj) { retVal = {}; for(i in obj){ - if(!(i in retVal) || retVal[i] != obj[i]) { + // https://issues.apache.org/jira/browse/CB-11522 'unknown' type may be returned in + // custom protocol activation case on Windows Phone 8.1 causing "No such interface supported" exception + // on cloning. + if((!(i in retVal) || retVal[i] != obj[i]) && typeof obj[i] != 'undefined' && typeof obj[i] != 'unknown') { retVal[i] = utils.clone(obj[i]); } } diff --git a/StoneIsland/platforms/android/platform_www/cordova_plugins.js b/StoneIsland/platforms/android/platform_www/cordova_plugins.js index a2734881..722b1683 100755 --- a/StoneIsland/platforms/android/platform_www/cordova_plugins.js +++ b/StoneIsland/platforms/android/platform_www/cordova_plugins.js @@ -104,6 +104,14 @@ module.exports = [ "cordova.plugins.Keyboard" ], "runs": true + }, + { + "id": "phonegap-plugin-push.PushNotification", + "file": "plugins/phonegap-plugin-push/www/push.js", + "pluginId": "phonegap-plugin-push", + "clobbers": [ + "PushNotification" + ] } ]; module.exports.metadata = @@ -121,7 +129,8 @@ module.exports.metadata = "cordova-plugin-splashscreen": "4.0.0", "cordova-plugin-compat": "1.1.0", "cordova-plugin-geolocation": "2.4.0", - "ionic-plugin-keyboard": "2.2.1" -} + "ionic-plugin-keyboard": "2.2.1", + "phonegap-plugin-push": "1.9.2" +}; // BOTTOM OF METADATA });
\ No newline at end of file diff --git a/StoneIsland/platforms/android/platform_www/plugins/phonegap-plugin-push/www/push.js b/StoneIsland/platforms/android/platform_www/plugins/phonegap-plugin-push/www/push.js new file mode 100644 index 00000000..a5315486 --- /dev/null +++ b/StoneIsland/platforms/android/platform_www/plugins/phonegap-plugin-push/www/push.js @@ -0,0 +1,329 @@ +cordova.define("phonegap-plugin-push.PushNotification", function(require, exports, module) { +/* global cordova:false */ +/* globals window */ + +/*! + * Module dependencies. + */ + +var exec = cordova.require('cordova/exec'); + +/** + * PushNotification constructor. + * + * @param {Object} options to initiate Push Notifications. + * @return {PushNotification} instance that can be monitored and cancelled. + */ + +var PushNotification = function(options) { + this._handlers = { + 'registration': [], + 'notification': [], + 'error': [] + }; + + // require options parameter + if (typeof options === 'undefined') { + throw new Error('The options argument is required.'); + } + + // store the options to this object instance + this.options = options; + + // triggered on registration and notification + var that = this; + var success = function(result) { + if (result && typeof result.registrationId !== 'undefined') { + that.emit('registration', result); + } else if (result && result.additionalData && typeof result.additionalData.actionCallback !== 'undefined') { + var executeFuctionOrEmitEventByName = function(callbackName, context, arg) { + var namespaces = callbackName.split('.'); + var func = namespaces.pop(); + for (var i = 0; i < namespaces.length; i++) { + context = context[namespaces[i]]; + } + + if (typeof context[func] === 'function') { + context[func].call(context, arg); + } else { + that.emit(callbackName, arg); + } + }; + + executeFuctionOrEmitEventByName(result.additionalData.actionCallback, window, result); + } else if (result) { + that.emit('notification', result); + } + }; + + // triggered on error + var fail = function(msg) { + var e = (typeof msg === 'string') ? new Error(msg) : msg; + that.emit('error', e); + }; + + // wait at least one process tick to allow event subscriptions + setTimeout(function() { + exec(success, fail, 'PushNotification', 'init', [options]); + }, 10); +}; + +/** + * Unregister from push notifications + */ + +PushNotification.prototype.unregister = function(successCallback, errorCallback, options) { + if (!errorCallback) { errorCallback = function() {}; } + + if (typeof errorCallback !== 'function') { + console.log('PushNotification.unregister failure: failure parameter not a function'); + return; + } + + if (typeof successCallback !== 'function') { + console.log('PushNotification.unregister failure: success callback parameter must be a function'); + return; + } + + var that = this; + var cleanHandlersAndPassThrough = function() { + if (!options) { + that._handlers = { + 'registration': [], + 'notification': [], + 'error': [] + }; + } + successCallback(); + }; + + exec(cleanHandlersAndPassThrough, errorCallback, 'PushNotification', 'unregister', [options]); +}; + +/** + * subscribe to a topic + * @param {String} topic topic to subscribe + * @param {Function} successCallback success callback + * @param {Function} errorCallback error callback + * @return {void} + */ +PushNotification.prototype.subscribe = function(topic, successCallback, errorCallback) { + if (!errorCallback) { errorCallback = function() {}; } + + if (typeof errorCallback !== 'function') { + console.log('PushNotification.subscribe failure: failure parameter not a function'); + return; + } + + if (typeof successCallback !== 'function') { + console.log('PushNotification.subscribe failure: success callback parameter must be a function'); + return; + } + + exec(successCallback, errorCallback, 'PushNotification', 'subscribe', [topic]); +}; + +/** + * unsubscribe to a topic + * @param {String} topic topic to unsubscribe + * @param {Function} successCallback success callback + * @param {Function} errorCallback error callback + * @return {void} + */ +PushNotification.prototype.unsubscribe = function(topic, successCallback, errorCallback) { + if (!errorCallback) { errorCallback = function() {}; } + + if (typeof errorCallback !== 'function') { + console.log('PushNotification.unsubscribe failure: failure parameter not a function'); + return; + } + + if (typeof successCallback !== 'function') { + console.log('PushNotification.unsubscribe failure: success callback parameter must be a function'); + return; + } + + exec(successCallback, errorCallback, 'PushNotification', 'unsubscribe', [topic]); +}; + +/** + * Call this to set the application icon badge + */ + +PushNotification.prototype.setApplicationIconBadgeNumber = function(successCallback, errorCallback, badge) { + if (!errorCallback) { errorCallback = function() {}; } + + if (typeof errorCallback !== 'function') { + console.log('PushNotification.setApplicationIconBadgeNumber failure: failure parameter not a function'); + return; + } + + if (typeof successCallback !== 'function') { + console.log('PushNotification.setApplicationIconBadgeNumber failure: success callback parameter must be a function'); + return; + } + + exec(successCallback, errorCallback, 'PushNotification', 'setApplicationIconBadgeNumber', [{badge: badge}]); +}; + +/** + * Get the application icon badge + */ + +PushNotification.prototype.getApplicationIconBadgeNumber = function(successCallback, errorCallback) { + if (!errorCallback) { errorCallback = function() {}; } + + if (typeof errorCallback !== 'function') { + console.log('PushNotification.getApplicationIconBadgeNumber failure: failure parameter not a function'); + return; + } + + if (typeof successCallback !== 'function') { + console.log('PushNotification.getApplicationIconBadgeNumber failure: success callback parameter must be a function'); + return; + } + + exec(successCallback, errorCallback, 'PushNotification', 'getApplicationIconBadgeNumber', []); +}; + +/** + * Get the application icon badge + */ + +PushNotification.prototype.clearAllNotifications = function(successCallback, errorCallback) { + if (!successCallback) { successCallback = function() {}; } + if (!errorCallback) { errorCallback = function() {}; } + + if (typeof errorCallback !== 'function') { + console.log('PushNotification.clearAllNotifications failure: failure parameter not a function'); + return; + } + + if (typeof successCallback !== 'function') { + console.log('PushNotification.clearAllNotifications failure: success callback parameter must be a function'); + return; + } + + exec(successCallback, errorCallback, 'PushNotification', 'clearAllNotifications', []); +}; + +/** + * Listen for an event. + * + * Any event is supported, but the following are built-in: + * + * - registration + * - notification + * - error + * + * @param {String} eventName to subscribe to. + * @param {Function} callback triggered on the event. + */ + +PushNotification.prototype.on = function(eventName, callback) { + if (!this._handlers.hasOwnProperty(eventName)) { + this._handlers[eventName] = []; + } + this._handlers[eventName].push(callback); +}; + +/** + * Remove event listener. + * + * @param {String} eventName to match subscription. + * @param {Function} handle function associated with event. + */ + +PushNotification.prototype.off = function (eventName, handle) { + if (this._handlers.hasOwnProperty(eventName)) { + var handleIndex = this._handlers[eventName].indexOf(handle); + if (handleIndex >= 0) { + this._handlers[eventName].splice(handleIndex, 1); + } + } +}; + +/** + * Emit an event. + * + * This is intended for internal use only. + * + * @param {String} eventName is the event to trigger. + * @param {*} all arguments are passed to the event listeners. + * + * @return {Boolean} is true when the event is triggered otherwise false. + */ + +PushNotification.prototype.emit = function() { + var args = Array.prototype.slice.call(arguments); + var eventName = args.shift(); + + if (!this._handlers.hasOwnProperty(eventName)) { + return false; + } + + for (var i = 0, length = this._handlers[eventName].length; i < length; i++) { + var callback = this._handlers[eventName][i]; + if (typeof callback === 'function') { + callback.apply(undefined,args); + } else { + console.log('event handler: ' + eventName + ' must be a function'); + } + } + + return true; +}; + +PushNotification.prototype.finish = function(successCallback, errorCallback, id) { + if (!successCallback) { successCallback = function() {}; } + if (!errorCallback) { errorCallback = function() {}; } + if (!id) { id = 'handler'; } + + if (typeof successCallback !== 'function') { + console.log('finish failure: success callback parameter must be a function'); + return; + } + + if (typeof errorCallback !== 'function') { + console.log('finish failure: failure parameter not a function'); + return; + } + + exec(successCallback, errorCallback, 'PushNotification', 'finish', [id]); +}; + +/*! + * Push Notification Plugin. + */ + +module.exports = { + /** + * Register for Push Notifications. + * + * This method will instantiate a new copy of the PushNotification object + * and start the registration process. + * + * @param {Object} options + * @return {PushNotification} instance + */ + + init: function(options) { + return new PushNotification(options); + }, + + hasPermission: function(successCallback, errorCallback) { + exec(successCallback, errorCallback, 'PushNotification', 'hasPermission', []); + }, + + /** + * PushNotification Object. + * + * Expose the PushNotification object for direct use + * and testing. Typically, you should use the + * .init helper method. + */ + + PushNotification: PushNotification +}; + +}); diff --git a/StoneIsland/platforms/android/project.properties b/StoneIsland/platforms/android/project.properties index b06f0fa0..6936daa2 100755 --- a/StoneIsland/platforms/android/project.properties +++ b/StoneIsland/platforms/android/project.properties @@ -10,5 +10,9 @@ # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt # Project target. -target=android-22 +target=android-25 android.library.reference.1=CordovaLib +cordova.gradle.include.1=phonegap-plugin-push/stoneisland-push.gradle +cordova.system.library.1=com.android.support:support-v13:23+ +cordova.system.library.2=com.google.android.gms:play-services-gcm:9.8+ +cordova.system.library.3=me.leolin:ShortcutBadger:1.1.11@aar
\ No newline at end of file diff --git a/StoneIsland/platforms/android/res/values/strings.xml b/StoneIsland/platforms/android/res/values/strings.xml index bd922fe9..60540e70 100755 --- a/StoneIsland/platforms/android/res/values/strings.xml +++ b/StoneIsland/platforms/android/res/values/strings.xml @@ -3,4 +3,5 @@ <string name="app_name">Stone Island</string> <string name="launcher_name">@string/app_name</string> <string name="activity_name">@string/launcher_name</string> + <string name="google_app_id">XXXXXXX</string> </resources> diff --git a/StoneIsland/platforms/android/res/xml/config.xml b/StoneIsland/platforms/android/res/xml/config.xml index b9330793..00961deb 100755..100644 --- a/StoneIsland/platforms/android/res/xml/config.xml +++ b/StoneIsland/platforms/android/res/xml/config.xml @@ -1,5 +1,5 @@ <?xml version='1.0' encoding='utf-8'?> -<widget id="us.okfoc.stoneisland" version="0.7.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0"> +<widget id="us.okfoc.stoneisland" version="0.8.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0"> <feature name="ParsePlugin"> <param name="android-package" value="org.apache.cordova.core.ParsePlugin" /> </feature> @@ -69,4 +69,7 @@ <preference name="AllowInlineMediaPlayback" value="true" /> <preference name="AndroidLaunchMode" value="singleTop" /> <preference name="android-minSdkVersion" value="21" /> + <feature name="PushNotification"> + <param name="android-package" value="com.adobe.phonegap.push.PushPlugin" /> + </feature> </widget> diff --git a/StoneIsland/platforms/android/src/com/adobe/phonegap/push/BackgroundActionButtonHandler.java b/StoneIsland/platforms/android/src/com/adobe/phonegap/push/BackgroundActionButtonHandler.java new file mode 100644 index 00000000..3ccea6cb --- /dev/null +++ b/StoneIsland/platforms/android/src/com/adobe/phonegap/push/BackgroundActionButtonHandler.java @@ -0,0 +1,41 @@ +package com.adobe.phonegap.push; + +import android.app.NotificationManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.support.v4.app.RemoteInput; + +public class BackgroundActionButtonHandler extends BroadcastReceiver implements PushConstants { + private static String LOG_TAG = "PushPlugin_BackgroundActionButtonHandler"; + + @Override + public void onReceive(Context context, Intent intent) { + Bundle extras = intent.getExtras(); + Log.d(LOG_TAG, "BackgroundActionButtonHandler = " + extras); + + int notId = intent.getIntExtra(NOT_ID, 0); + Log.d(LOG_TAG, "not id = " + notId); + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(GCMIntentService.getAppName(context), notId); + + if (extras != null) { + Bundle originalExtras = extras.getBundle(PUSH_BUNDLE); + + originalExtras.putBoolean(FOREGROUND, false); + originalExtras.putBoolean(COLDSTART, false); + originalExtras.putString(ACTION_CALLBACK, extras.getString(CALLBACK)); + + Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); + if (remoteInput != null) { + String inputString = remoteInput.getCharSequence(INLINE_REPLY).toString(); + Log.d(LOG_TAG, "response: " + inputString); + originalExtras.putString(INLINE_REPLY, inputString); + } + + PushPlugin.sendExtras(originalExtras); + } + } +} diff --git a/StoneIsland/platforms/android/src/com/adobe/phonegap/push/GCMIntentService.java b/StoneIsland/platforms/android/src/com/adobe/phonegap/push/GCMIntentService.java new file mode 100644 index 00000000..e1a2b75c --- /dev/null +++ b/StoneIsland/platforms/android/src/com/adobe/phonegap/push/GCMIntentService.java @@ -0,0 +1,802 @@ +package com.adobe.phonegap.push; + +import android.annotation.SuppressLint; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationManagerCompat; +import android.support.v4.app.NotificationCompat.WearableExtender; +import android.support.v4.app.RemoteInput; +import android.text.Html; +import android.text.Spanned; +import android.util.Log; + +import com.google.android.gms.gcm.GcmListenerService; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Random; + +@SuppressLint("NewApi") +public class GCMIntentService extends GcmListenerService implements PushConstants { + + private static final String LOG_TAG = "PushPlugin_GCMIntentService"; + private static HashMap<Integer, ArrayList<String>> messageMap = new HashMap<Integer, ArrayList<String>>(); + + public void setNotification(int notId, String message){ + ArrayList<String> messageList = messageMap.get(notId); + if(messageList == null) { + messageList = new ArrayList<String>(); + messageMap.put(notId, messageList); + } + + if(message.isEmpty()){ + messageList.clear(); + }else{ + messageList.add(message); + } + } + + @Override + public void onMessageReceived(String from, Bundle extras) { + Log.d(LOG_TAG, "onMessage - from: " + from); + + if (extras != null) { + Context applicationContext = getApplicationContext(); + + SharedPreferences prefs = applicationContext.getSharedPreferences(PushPlugin.COM_ADOBE_PHONEGAP_PUSH, Context.MODE_PRIVATE); + boolean forceShow = prefs.getBoolean(FORCE_SHOW, false); + boolean clearBadge = prefs.getBoolean(CLEAR_BADGE, false); + + extras = normalizeExtras(applicationContext, extras); + + if (clearBadge) { + PushPlugin.setApplicationIconBadgeNumber(getApplicationContext(), 0); + } + + // if we are in the foreground and forceShow is `false` only send data + if (!forceShow && PushPlugin.isInForeground()) { + Log.d(LOG_TAG, "foreground"); + extras.putBoolean(FOREGROUND, true); + extras.putBoolean(COLDSTART, false); + PushPlugin.sendExtras(extras); + } + // if we are in the foreground and forceShow is `true`, force show the notification if the data has at least a message or title + else if (forceShow && PushPlugin.isInForeground()) { + Log.d(LOG_TAG, "foreground force"); + extras.putBoolean(FOREGROUND, true); + extras.putBoolean(COLDSTART, false); + + showNotificationIfPossible(applicationContext, extras); + } + // if we are not in the foreground always send notification if the data has at least a message or title + else { + Log.d(LOG_TAG, "background"); + extras.putBoolean(FOREGROUND, false); + extras.putBoolean(COLDSTART, PushPlugin.isActive()); + + showNotificationIfPossible(applicationContext, extras); + } + } + } + + /* + * Change a values key in the extras bundle + */ + private void replaceKey(Context context, String oldKey, String newKey, Bundle extras, Bundle newExtras) { + Object value = extras.get(oldKey); + if ( value != null ) { + if (value instanceof String) { + value = localizeKey(context, newKey, (String) value); + + newExtras.putString(newKey, (String) value); + } else if (value instanceof Boolean) { + newExtras.putBoolean(newKey, (Boolean) value); + } else if (value instanceof Number) { + newExtras.putDouble(newKey, ((Number) value).doubleValue()); + } else { + newExtras.putString(newKey, String.valueOf(value)); + } + } + } + + /* + * Normalize localization for key + */ + private String localizeKey(Context context, String key, String value) { + if (key.equals(TITLE) || key.equals(MESSAGE) || key.equals(SUMMARY_TEXT)) { + try { + JSONObject localeObject = new JSONObject(value); + + String localeKey = localeObject.getString(LOC_KEY); + + ArrayList<String> localeFormatData = new ArrayList<String>(); + if (!localeObject.isNull(LOC_DATA)) { + String localeData = localeObject.getString(LOC_DATA); + JSONArray localeDataArray = new JSONArray(localeData); + for (int i = 0 ; i < localeDataArray.length(); i++) { + localeFormatData.add(localeDataArray.getString(i)); + } + } + + String packageName = context.getPackageName(); + Resources resources = context.getResources(); + + int resourceId = resources.getIdentifier(localeKey, "string", packageName); + + if (resourceId != 0) { + return resources.getString(resourceId, localeFormatData.toArray()); + } + else { + Log.d(LOG_TAG, "can't find resource for locale key = " + localeKey); + + return value; + } + } + catch(JSONException e) { + Log.d(LOG_TAG, "no locale found for key = " + key + ", error " + e.getMessage()); + + return value; + } + } + + return value; + } + + /* + * Replace alternate keys with our canonical value + */ + private String normalizeKey(String key) { + if (key.equals(BODY) || key.equals(ALERT) || key.equals(GCM_NOTIFICATION_BODY) || key.equals(TWILIO_BODY)) { + return MESSAGE; + } else if (key.equals(TWILIO_TITLE)) { + return TITLE; + }else if (key.equals(MSGCNT) || key.equals(BADGE)) { + return COUNT; + } else if (key.equals(SOUNDNAME) || key.equals(TWILIO_SOUND)) { + return SOUND; + } else if (key.startsWith(GCM_NOTIFICATION)) { + return key.substring(GCM_NOTIFICATION.length()+1, key.length()); + } else if (key.startsWith(GCM_N)) { + return key.substring(GCM_N.length()+1, key.length()); + } else if (key.startsWith(UA_PREFIX)) { + key = key.substring(UA_PREFIX.length()+1, key.length()); + return key.toLowerCase(); + } else { + return key; + } + } + + /* + * Parse bundle into normalized keys. + */ + private Bundle normalizeExtras(Context context, Bundle extras) { + Log.d(LOG_TAG, "normalize extras"); + Iterator<String> it = extras.keySet().iterator(); + Bundle newExtras = new Bundle(); + + while (it.hasNext()) { + String key = it.next(); + + Log.d(LOG_TAG, "key = " + key); + + // If normalizeKeythe key is "data" or "message" and the value is a json object extract + // This is to support parse.com and other services. Issue #147 and pull #218 + if (key.equals(PARSE_COM_DATA) || key.equals(MESSAGE)) { + Object json = extras.get(key); + // Make sure data is json object stringified + if ( json instanceof String && ((String) json).startsWith("{") ) { + Log.d(LOG_TAG, "extracting nested message data from key = " + key); + try { + // If object contains message keys promote each value to the root of the bundle + JSONObject data = new JSONObject((String) json); + if ( data.has(ALERT) || data.has(MESSAGE) || data.has(BODY) || data.has(TITLE) ) { + Iterator<String> jsonIter = data.keys(); + while (jsonIter.hasNext()) { + String jsonKey = jsonIter.next(); + + Log.d(LOG_TAG, "key = data/" + jsonKey); + + String value = data.getString(jsonKey); + jsonKey = normalizeKey(jsonKey); + value = localizeKey(context, jsonKey, value); + + newExtras.putString(jsonKey, value); + } + } + } catch( JSONException e) { + Log.e(LOG_TAG, "normalizeExtras: JSON exception"); + } + } + } else if (key.equals(("notification"))) { + Bundle value = extras.getBundle(key); + Iterator<String> iterator = value.keySet().iterator(); + while (iterator.hasNext()) { + String notifkey = iterator.next(); + + Log.d(LOG_TAG, "notifkey = " + notifkey); + String newKey = normalizeKey(notifkey); + Log.d(LOG_TAG, "replace key " + notifkey + " with " + newKey); + + String valueData = value.getString(notifkey); + valueData = localizeKey(context, newKey, valueData); + + newExtras.putString(newKey, valueData); + } + continue; + } + + String newKey = normalizeKey(key); + Log.d(LOG_TAG, "replace key " + key + " with " + newKey); + replaceKey(context, key, newKey, extras, newExtras); + + } // while + + return newExtras; + } + + private int extractBadgeCount(Bundle extras) { + int count = -1; + String msgcnt = extras.getString(COUNT); + + try { + if (msgcnt != null) { + count = Integer.parseInt(msgcnt); + } + } catch (NumberFormatException e) { + Log.e(LOG_TAG, e.getLocalizedMessage(), e); + } + + return count; + } + + private void showNotificationIfPossible (Context context, Bundle extras) { + + // Send a notification if there is a message or title, otherwise just send data + String message = extras.getString(MESSAGE); + String title = extras.getString(TITLE); + String contentAvailable = extras.getString(CONTENT_AVAILABLE); + String forceStart = extras.getString(FORCE_START); + int badgeCount = extractBadgeCount(extras); + if (badgeCount >= 0) { + Log.d(LOG_TAG, "count =[" + badgeCount + "]"); + PushPlugin.setApplicationIconBadgeNumber(context, badgeCount); + } + + Log.d(LOG_TAG, "message =[" + message + "]"); + Log.d(LOG_TAG, "title =[" + title + "]"); + Log.d(LOG_TAG, "contentAvailable =[" + contentAvailable + "]"); + Log.d(LOG_TAG, "forceStart =[" + forceStart + "]"); + + if ((message != null && message.length() != 0) || + (title != null && title.length() != 0)) { + + Log.d(LOG_TAG, "create notification"); + + if(title == null || title.isEmpty()){ + extras.putString(TITLE, getAppName(this)); + } + + createNotification(context, extras); + } + + if(!PushPlugin.isActive() && "1".equals(forceStart)){ + Log.d(LOG_TAG, "app is not running but we should start it and put in background"); + Intent intent = new Intent(this, PushHandlerActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(PUSH_BUNDLE, extras); + intent.putExtra(START_IN_BACKGROUND, true); + intent.putExtra(FOREGROUND, false); + startActivity(intent); + } else if ("1".equals(contentAvailable)) { + Log.d(LOG_TAG, "app is not running and content available true"); + Log.d(LOG_TAG, "send notification event"); + PushPlugin.sendExtras(extras); + } + } + + public void createNotification(Context context, Bundle extras) { + NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + String appName = getAppName(this); + String packageName = context.getPackageName(); + Resources resources = context.getResources(); + + int notId = parseInt(NOT_ID, extras); + Intent notificationIntent = new Intent(this, PushHandlerActivity.class); + notificationIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); + notificationIntent.putExtra(PUSH_BUNDLE, extras); + notificationIntent.putExtra(NOT_ID, notId); + + int requestCode = new Random().nextInt(); + PendingIntent contentIntent = PendingIntent.getActivity(this, requestCode, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + NotificationCompat.Builder mBuilder = + new NotificationCompat.Builder(context) + .setWhen(System.currentTimeMillis()) + .setContentTitle(fromHtml(extras.getString(TITLE))) + .setTicker(fromHtml(extras.getString(TITLE))) + .setContentIntent(contentIntent) + .setAutoCancel(true); + + SharedPreferences prefs = context.getSharedPreferences(PushPlugin.COM_ADOBE_PHONEGAP_PUSH, Context.MODE_PRIVATE); + String localIcon = prefs.getString(ICON, null); + String localIconColor = prefs.getString(ICON_COLOR, null); + boolean soundOption = prefs.getBoolean(SOUND, true); + boolean vibrateOption = prefs.getBoolean(VIBRATE, true); + Log.d(LOG_TAG, "stored icon=" + localIcon); + Log.d(LOG_TAG, "stored iconColor=" + localIconColor); + Log.d(LOG_TAG, "stored sound=" + soundOption); + Log.d(LOG_TAG, "stored vibrate=" + vibrateOption); + + /* + * Notification Vibration + */ + + setNotificationVibration(extras, vibrateOption, mBuilder); + + /* + * Notification Icon Color + * + * Sets the small-icon background color of the notification. + * To use, add the `iconColor` key to plugin android options + * + */ + setNotificationIconColor(extras.getString("color"), mBuilder, localIconColor); + + /* + * Notification Icon + * + * Sets the small-icon of the notification. + * + * - checks the plugin options for `icon` key + * - if none, uses the application icon + * + * The icon value must be a string that maps to a drawable resource. + * If no resource is found, falls + * + */ + setNotificationSmallIcon(context, extras, packageName, resources, mBuilder, localIcon); + + /* + * Notification Large-Icon + * + * Sets the large-icon of the notification + * + * - checks the gcm data for the `image` key + * - checks to see if remote image, loads it. + * - checks to see if assets image, Loads It. + * - checks to see if resource image, LOADS IT! + * - if none, we don't set the large icon + * + */ + setNotificationLargeIcon(extras, packageName, resources, mBuilder); + + /* + * Notification Sound + */ + if (soundOption) { + setNotificationSound(context, extras, mBuilder); + } + + /* + * LED Notification + */ + setNotificationLedColor(extras, mBuilder); + + /* + * Priority Notification + */ + setNotificationPriority(extras, mBuilder); + + /* + * Notification message + */ + setNotificationMessage(notId, extras, mBuilder); + + /* + * Notification count + */ + setNotificationCount(context, extras, mBuilder); + + /* + * Notification count + */ + setVisibility(context, extras, mBuilder); + + /* + * Notification add actions + */ + createActions(extras, mBuilder, resources, packageName, notId); + + mNotificationManager.notify(appName, notId, mBuilder.build()); + } + + private void updateIntent(Intent intent, String callback, Bundle extras, boolean foreground, int notId) { + intent.putExtra(CALLBACK, callback); + intent.putExtra(PUSH_BUNDLE, extras); + intent.putExtra(FOREGROUND, foreground); + intent.putExtra(NOT_ID, notId); + } + + private void createActions(Bundle extras, NotificationCompat.Builder mBuilder, Resources resources, String packageName, int notId) { + Log.d(LOG_TAG, "create actions: with in-line"); + String actions = extras.getString(ACTIONS); + if (actions != null) { + try { + JSONArray actionsArray = new JSONArray(actions); + ArrayList<NotificationCompat.Action> wActions = new ArrayList<NotificationCompat.Action>(); + for (int i=0; i < actionsArray.length(); i++) { + int min = 1; + int max = 2000000000; + Random random = new Random(); + int uniquePendingIntentRequestCode = random.nextInt((max - min) + 1) + min; + Log.d(LOG_TAG, "adding action"); + JSONObject action = actionsArray.getJSONObject(i); + Log.d(LOG_TAG, "adding callback = " + action.getString(CALLBACK)); + boolean foreground = action.optBoolean(FOREGROUND, true); + boolean inline = action.optBoolean("inline", false); + Intent intent = null; + PendingIntent pIntent = null; + if (inline) { + Log.d(LOG_TAG, "Version: " + android.os.Build.VERSION.SDK_INT + " = " + android.os.Build.VERSION_CODES.M); + if (android.os.Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.M) { + Log.d(LOG_TAG, "push activity"); + intent = new Intent(this, PushHandlerActivity.class); + } else { + Log.d(LOG_TAG, "push receiver"); + intent = new Intent(this, BackgroundActionButtonHandler.class); + } + + updateIntent(intent, action.getString(CALLBACK), extras, foreground, notId); + + if (android.os.Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.M) { + Log.d(LOG_TAG, "push activity for notId " + notId); + pIntent = PendingIntent.getActivity(this, uniquePendingIntentRequestCode, intent, PendingIntent.FLAG_ONE_SHOT); + } else { + Log.d(LOG_TAG, "push receiver for notId " + notId); + pIntent = PendingIntent.getBroadcast(this, uniquePendingIntentRequestCode, intent, PendingIntent.FLAG_ONE_SHOT); + } + } else if (foreground) { + intent = new Intent(this, PushHandlerActivity.class); + updateIntent(intent, action.getString(CALLBACK), extras, foreground, notId); + pIntent = PendingIntent.getActivity(this, uniquePendingIntentRequestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } else { + intent = new Intent(this, BackgroundActionButtonHandler.class); + updateIntent(intent, action.getString(CALLBACK), extras, foreground, notId); + pIntent = PendingIntent.getBroadcast(this, uniquePendingIntentRequestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + NotificationCompat.Action.Builder actionBuilder = + new NotificationCompat.Action.Builder(resources.getIdentifier(action.optString(ICON, ""), DRAWABLE, packageName), + action.getString(TITLE), pIntent); + + RemoteInput remoteInput = null; + if (inline) { + Log.d(LOG_TAG, "create remote input"); + String replyLabel = "Enter your reply here"; + remoteInput = + new RemoteInput.Builder(INLINE_REPLY) + .setLabel(replyLabel) + .build(); + actionBuilder.addRemoteInput(remoteInput); + } + + NotificationCompat.Action wAction = actionBuilder.build(); + wActions.add(actionBuilder.build()); + + if (inline) { + mBuilder.addAction(wAction); + } else { + mBuilder.addAction(resources.getIdentifier(action.optString(ICON, ""), DRAWABLE, packageName), + action.getString(TITLE), pIntent); + } + wAction = null; + pIntent = null; + } + mBuilder.extend(new WearableExtender().addActions(wActions)); + wActions.clear(); + } catch(JSONException e) { + // nope + } + } + } + + private void setNotificationCount(Context context, Bundle extras, NotificationCompat.Builder mBuilder) { + int count = extractBadgeCount(extras); + if (count >= 0) { + Log.d(LOG_TAG, "count =[" + count + "]"); + mBuilder.setNumber(count); + } + } + + + private void setVisibility(Context context, Bundle extras, NotificationCompat.Builder mBuilder) { + String visibilityStr = extras.getString(VISIBILITY); + if (visibilityStr != null) { + try { + Integer visibility = Integer.parseInt(visibilityStr); + if (visibility >= NotificationCompat.VISIBILITY_SECRET && visibility <= NotificationCompat.VISIBILITY_PUBLIC) { + mBuilder.setVisibility(visibility); + } else { + Log.e(LOG_TAG, "Visibility parameter must be between -1 and 1"); + } + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } + } + + private void setNotificationVibration(Bundle extras, Boolean vibrateOption, NotificationCompat.Builder mBuilder) { + String vibrationPattern = extras.getString(VIBRATION_PATTERN); + if (vibrationPattern != null) { + String[] items = vibrationPattern.replaceAll("\\[", "").replaceAll("\\]", "").split(","); + long[] results = new long[items.length]; + for (int i = 0; i < items.length; i++) { + try { + results[i] = Long.parseLong(items[i].trim()); + } catch (NumberFormatException nfe) {} + } + mBuilder.setVibrate(results); + } else { + if (vibrateOption) { + mBuilder.setDefaults(Notification.DEFAULT_VIBRATE); + } + } + } + + private void setNotificationMessage(int notId, Bundle extras, NotificationCompat.Builder mBuilder) { + String message = extras.getString(MESSAGE); + + String style = extras.getString(STYLE, STYLE_TEXT); + if(STYLE_INBOX.equals(style)) { + setNotification(notId, message); + + mBuilder.setContentText(fromHtml(message)); + + ArrayList<String> messageList = messageMap.get(notId); + Integer sizeList = messageList.size(); + if (sizeList > 1) { + String sizeListMessage = sizeList.toString(); + String stacking = sizeList + " more"; + if (extras.getString(SUMMARY_TEXT) != null) { + stacking = extras.getString(SUMMARY_TEXT); + stacking = stacking.replace("%n%", sizeListMessage); + } + NotificationCompat.InboxStyle notificationInbox = new NotificationCompat.InboxStyle() + .setBigContentTitle(fromHtml(extras.getString(TITLE))) + .setSummaryText(fromHtml(stacking)); + + for (int i = messageList.size() - 1; i >= 0; i--) { + notificationInbox.addLine(fromHtml(messageList.get(i))); + } + + mBuilder.setStyle(notificationInbox); + } else { + NotificationCompat.BigTextStyle bigText = new NotificationCompat.BigTextStyle(); + if (message != null) { + bigText.bigText(fromHtml(message)); + bigText.setBigContentTitle(fromHtml(extras.getString(TITLE))); + mBuilder.setStyle(bigText); + } + } + } else if (STYLE_PICTURE.equals(style)) { + setNotification(notId, ""); + + NotificationCompat.BigPictureStyle bigPicture = new NotificationCompat.BigPictureStyle(); + bigPicture.bigPicture(getBitmapFromURL(extras.getString(PICTURE))); + bigPicture.setBigContentTitle(fromHtml(extras.getString(TITLE))); + bigPicture.setSummaryText(fromHtml(extras.getString(SUMMARY_TEXT))); + + mBuilder.setContentTitle(fromHtml(extras.getString(TITLE))); + mBuilder.setContentText(fromHtml(message)); + + mBuilder.setStyle(bigPicture); + } else { + setNotification(notId, ""); + + NotificationCompat.BigTextStyle bigText = new NotificationCompat.BigTextStyle(); + + if (message != null) { + mBuilder.setContentText(fromHtml(message)); + + bigText.bigText(fromHtml(message)); + bigText.setBigContentTitle(fromHtml(extras.getString(TITLE))); + + String summaryText = extras.getString(SUMMARY_TEXT); + if (summaryText != null) { + bigText.setSummaryText(fromHtml(summaryText)); + } + + mBuilder.setStyle(bigText); + } + /* + else { + mBuilder.setContentText("<missing message content>"); + } + */ + } + } + + private void setNotificationSound(Context context, Bundle extras, NotificationCompat.Builder mBuilder) { + String soundname = extras.getString(SOUNDNAME); + if (soundname == null) { + soundname = extras.getString(SOUND); + } + if (SOUND_RINGTONE.equals(soundname)) { + mBuilder.setSound(android.provider.Settings.System.DEFAULT_RINGTONE_URI); + } else if (soundname != null && !soundname.contentEquals(SOUND_DEFAULT)) { + Uri sound = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + + "://" + context.getPackageName() + "/raw/" + soundname); + Log.d(LOG_TAG, sound.toString()); + mBuilder.setSound(sound); + } else { + mBuilder.setSound(android.provider.Settings.System.DEFAULT_NOTIFICATION_URI); + } + } + + private void setNotificationLedColor(Bundle extras, NotificationCompat.Builder mBuilder) { + String ledColor = extras.getString(LED_COLOR); + if (ledColor != null) { + // Converts parse Int Array from ledColor + String[] items = ledColor.replaceAll("\\[", "").replaceAll("\\]", "").split(","); + int[] results = new int[items.length]; + for (int i = 0; i < items.length; i++) { + try { + results[i] = Integer.parseInt(items[i].trim()); + } catch (NumberFormatException nfe) {} + } + if (results.length == 4) { + mBuilder.setLights(Color.argb(results[0], results[1], results[2], results[3]), 500, 500); + } else { + Log.e(LOG_TAG, "ledColor parameter must be an array of length == 4 (ARGB)"); + } + } + } + + private void setNotificationPriority(Bundle extras, NotificationCompat.Builder mBuilder) { + String priorityStr = extras.getString(PRIORITY); + if (priorityStr != null) { + try { + Integer priority = Integer.parseInt(priorityStr); + if (priority >= NotificationCompat.PRIORITY_MIN && priority <= NotificationCompat.PRIORITY_MAX) { + mBuilder.setPriority(priority); + } else { + Log.e(LOG_TAG, "Priority parameter must be between -2 and 2"); + } + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } + } + + private void setNotificationLargeIcon(Bundle extras, String packageName, Resources resources, NotificationCompat.Builder mBuilder) { + String gcmLargeIcon = extras.getString(IMAGE); // from gcm + if (gcmLargeIcon != null && !"".equals(gcmLargeIcon)) { + if (gcmLargeIcon.startsWith("http://") || gcmLargeIcon.startsWith("https://")) { + mBuilder.setLargeIcon(getBitmapFromURL(gcmLargeIcon)); + Log.d(LOG_TAG, "using remote large-icon from gcm"); + } else { + AssetManager assetManager = getAssets(); + InputStream istr; + try { + istr = assetManager.open(gcmLargeIcon); + Bitmap bitmap = BitmapFactory.decodeStream(istr); + mBuilder.setLargeIcon(bitmap); + Log.d(LOG_TAG, "using assets large-icon from gcm"); + } catch (IOException e) { + int largeIconId = 0; + largeIconId = resources.getIdentifier(gcmLargeIcon, DRAWABLE, packageName); + if (largeIconId != 0) { + Bitmap largeIconBitmap = BitmapFactory.decodeResource(resources, largeIconId); + mBuilder.setLargeIcon(largeIconBitmap); + Log.d(LOG_TAG, "using resources large-icon from gcm"); + } else { + Log.d(LOG_TAG, "Not setting large icon"); + } + } + } + } + } + + private void setNotificationSmallIcon(Context context, Bundle extras, String packageName, Resources resources, NotificationCompat.Builder mBuilder, String localIcon) { + int iconId = 0; + String icon = extras.getString(ICON); + if (icon != null && !"".equals(icon)) { + iconId = resources.getIdentifier(icon, DRAWABLE, packageName); + Log.d(LOG_TAG, "using icon from plugin options"); + } + else if (localIcon != null && !"".equals(localIcon)) { + iconId = resources.getIdentifier(localIcon, DRAWABLE, packageName); + Log.d(LOG_TAG, "using icon from plugin options"); + } + if (iconId == 0) { + Log.d(LOG_TAG, "no icon resource found - using application icon"); + iconId = context.getApplicationInfo().icon; + } + mBuilder.setSmallIcon(iconId); + } + + private void setNotificationIconColor(String color, NotificationCompat.Builder mBuilder, String localIconColor) { + int iconColor = 0; + if (color != null && !"".equals(color)) { + try { + iconColor = Color.parseColor(color); + } catch (IllegalArgumentException e) { + Log.e(LOG_TAG, "couldn't parse color from android options"); + } + } + else if (localIconColor != null && !"".equals(localIconColor)) { + try { + iconColor = Color.parseColor(localIconColor); + } catch (IllegalArgumentException e) { + Log.e(LOG_TAG, "couldn't parse color from android options"); + } + } + if (iconColor != 0) { + mBuilder.setColor(iconColor); + } + } + + public Bitmap getBitmapFromURL(String strURL) { + try { + URL url = new URL(strURL); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoInput(true); + connection.connect(); + InputStream input = connection.getInputStream(); + return BitmapFactory.decodeStream(input); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + public static String getAppName(Context context) { + CharSequence appName = context.getPackageManager().getApplicationLabel(context.getApplicationInfo()); + return (String)appName; + } + + private int parseInt(String value, Bundle extras) { + int retval = 0; + + try { + retval = Integer.parseInt(extras.getString(value)); + } + catch(NumberFormatException e) { + Log.e(LOG_TAG, "Number format exception - Error parsing " + value + ": " + e.getMessage()); + } + catch(Exception e) { + Log.e(LOG_TAG, "Number format exception - Error parsing " + value + ": " + e.getMessage()); + } + + return retval; + } + + private Spanned fromHtml(String source) { + if (source != null) + return Html.fromHtml(source); + else + return null; + } +} diff --git a/StoneIsland/platforms/android/src/com/adobe/phonegap/push/PermissionUtils.java b/StoneIsland/platforms/android/src/com/adobe/phonegap/push/PermissionUtils.java new file mode 100644 index 00000000..6aa5c9bf --- /dev/null +++ b/StoneIsland/platforms/android/src/com/adobe/phonegap/push/PermissionUtils.java @@ -0,0 +1,55 @@ +package com.adobe.phonegap.push; + +import android.content.Context; +import android.content.pm.ApplicationInfo; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class PermissionUtils { + + private static final String CHECK_OP_NO_THROW = "checkOpNoThrow"; + + public static boolean hasPermission(Context appContext, String appOpsServiceId) throws UnknownError { + + ApplicationInfo appInfo = appContext.getApplicationInfo(); + + String pkg = appContext.getPackageName(); + int uid = appInfo.uid; + Class appOpsClass = null; + Object appOps = appContext.getSystemService("appops"); + + try { + + appOpsClass = Class.forName("android.app.AppOpsManager"); + + Method checkOpNoThrowMethod = appOpsClass.getMethod( + CHECK_OP_NO_THROW, + Integer.TYPE, + Integer.TYPE, + String.class + ); + + Field opValue = appOpsClass.getDeclaredField(appOpsServiceId); + + int value = (int) opValue.getInt(Integer.class); + Object result = checkOpNoThrowMethod.invoke(appOps, value, uid, pkg); + + return Integer.parseInt(result.toString()) == 0; // AppOpsManager.MODE_ALLOWED + + } catch (ClassNotFoundException e) { + throw new UnknownError("class not found"); + } catch (NoSuchMethodException e) { + throw new UnknownError("no such method"); + } catch (NoSuchFieldException e) { + throw new UnknownError("no such field"); + } catch (InvocationTargetException e) { + throw new UnknownError("invocation target"); + } catch (IllegalAccessException e) { + throw new UnknownError("illegal access"); + } + + } + +} diff --git a/StoneIsland/platforms/android/src/com/adobe/phonegap/push/PushConstants.java b/StoneIsland/platforms/android/src/com/adobe/phonegap/push/PushConstants.java new file mode 100644 index 00000000..37874e04 --- /dev/null +++ b/StoneIsland/platforms/android/src/com/adobe/phonegap/push/PushConstants.java @@ -0,0 +1,72 @@ +package com.adobe.phonegap.push; + +public interface PushConstants { + public static final String COM_ADOBE_PHONEGAP_PUSH = "com.adobe.phonegap.push"; + public static final String REGISTRATION_ID = "registrationId"; + public static final String FOREGROUND = "foreground"; + public static final String TITLE = "title"; + public static final String NOT_ID = "notId"; + public static final String PUSH_BUNDLE = "pushBundle"; + public static final String ICON = "icon"; + public static final String ICON_COLOR = "iconColor"; + public static final String SOUND = "sound"; + public static final String SOUND_DEFAULT = "default"; + public static final String SOUND_RINGTONE = "ringtone"; + public static final String VIBRATE = "vibrate"; + public static final String ACTIONS = "actions"; + public static final String CALLBACK = "callback"; + public static final String ACTION_CALLBACK = "actionCallback"; + public static final String DRAWABLE = "drawable"; + public static final String MSGCNT = "msgcnt"; + public static final String VIBRATION_PATTERN = "vibrationPattern"; + public static final String STYLE = "style"; + public static final String SUMMARY_TEXT = "summaryText"; + public static final String PICTURE = "picture"; + public static final String GCM_N = "gcm.n."; + public static final String GCM_NOTIFICATION = "gcm.notification"; + public static final String GCM_NOTIFICATION_BODY = "gcm.notification.body"; + public static final String UA_PREFIX = "com.urbanairship.push"; + public static final String PARSE_COM_DATA = "data"; + public static final String ALERT = "alert"; + public static final String MESSAGE = "message"; + public static final String BODY = "body"; + public static final String SOUNDNAME = "soundname"; + public static final String LED_COLOR = "ledColor"; + public static final String PRIORITY = "priority"; + public static final String IMAGE = "image"; + public static final String STYLE_INBOX = "inbox"; + public static final String STYLE_PICTURE = "picture"; + public static final String STYLE_TEXT = "text"; + public static final String BADGE = "badge"; + public static final String INITIALIZE = "init"; + public static final String SUBSCRIBE = "subscribe"; + public static final String UNSUBSCRIBE = "unsubscribe"; + public static final String UNREGISTER = "unregister"; + public static final String EXIT = "exit"; + public static final String FINISH = "finish"; + public static final String HAS_PERMISSION = "hasPermission"; + public static final String ANDROID = "android"; + public static final String SENDER_ID = "senderID"; + public static final String CLEAR_BADGE = "clearBadge"; + public static final String CLEAR_NOTIFICATIONS = "clearNotifications"; + public static final String COLDSTART = "coldstart"; + public static final String ADDITIONAL_DATA = "additionalData"; + public static final String COUNT = "count"; + public static final String FROM = "from"; + public static final String COLLAPSE_KEY = "collapse_key"; + public static final String FORCE_SHOW = "forceShow"; + public static final String GCM = "GCM"; + public static final String CONTENT_AVAILABLE = "content-available"; + public static final String TOPICS = "topics"; + public static final String SET_APPLICATION_ICON_BADGE_NUMBER = "setApplicationIconBadgeNumber"; + public static final String CLEAR_ALL_NOTIFICATIONS = "clearAllNotifications"; + public static final String VISIBILITY = "visibility"; + public static final String INLINE_REPLY = "inlineReply"; + public static final String LOC_KEY = "locKey"; + public static final String LOC_DATA = "locData"; + public static final String TWILIO_BODY = "twi_body"; + public static final String TWILIO_TITLE = "twi_title"; + public static final String TWILIO_SOUND = "twi_sound"; + public static final String START_IN_BACKGROUND = "cdvStartInBackground"; + public static final String FORCE_START = "force-start"; +} diff --git a/StoneIsland/platforms/android/src/com/adobe/phonegap/push/PushHandlerActivity.java b/StoneIsland/platforms/android/src/com/adobe/phonegap/push/PushHandlerActivity.java new file mode 100644 index 00000000..23682ac8 --- /dev/null +++ b/StoneIsland/platforms/android/src/com/adobe/phonegap/push/PushHandlerActivity.java @@ -0,0 +1,120 @@ +package com.adobe.phonegap.push; + +import android.app.Activity; +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.util.Log; +import android.support.v4.app.RemoteInput; + + +public class PushHandlerActivity extends Activity implements PushConstants { + private static String LOG_TAG = "PushPlugin_PushHandlerActivity"; + + /* + * this activity will be started if the user touches a notification that we own. + * We send it's data off to the push plugin for processing. + * If needed, we boot up the main activity to kickstart the application. + * @see android.app.Activity#onCreate(android.os.Bundle) + */ + @Override + public void onCreate(Bundle savedInstanceState) { + GCMIntentService gcm = new GCMIntentService(); + + Intent intent = getIntent(); + + int notId = intent.getExtras().getInt(NOT_ID, 0); + Log.d(LOG_TAG, "not id = " + notId); + gcm.setNotification(notId, ""); + super.onCreate(savedInstanceState); + Log.v(LOG_TAG, "onCreate"); + String callback = getIntent().getExtras().getString("callback"); + Log.d(LOG_TAG, "callback = " + callback); + boolean foreground = getIntent().getExtras().getBoolean("foreground", true); + boolean startOnBackground = getIntent().getExtras().getBoolean(START_IN_BACKGROUND, false); + + if(!startOnBackground){ + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(GCMIntentService.getAppName(this), notId); + } + + boolean isPushPluginActive = PushPlugin.isActive(); + boolean inline = processPushBundle(isPushPluginActive, intent); + + if(inline && android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.N){ + foreground = true; + } + + Log.d(LOG_TAG, "bringToForeground = " + foreground); + + finish(); + + Log.d(LOG_TAG, "isPushPluginActive = " + isPushPluginActive); + if (!isPushPluginActive && foreground && inline) { + Log.d(LOG_TAG, "forceMainActivityReload"); + forceMainActivityReload(false); + } else if(startOnBackground) { + Log.d(LOG_TAG, "startOnBackgroundTrue"); + forceMainActivityReload(true); + } else { + Log.d(LOG_TAG, "don't want main activity"); + } + } + + /** + * Takes the pushBundle extras from the intent, + * and sends it through to the PushPlugin for processing. + */ + private boolean processPushBundle(boolean isPushPluginActive, Intent intent) { + Bundle extras = getIntent().getExtras(); + Bundle remoteInput = null; + + if (extras != null) { + Bundle originalExtras = extras.getBundle(PUSH_BUNDLE); + + originalExtras.putBoolean(FOREGROUND, false); + originalExtras.putBoolean(COLDSTART, !isPushPluginActive); + originalExtras.putString(ACTION_CALLBACK, extras.getString(CALLBACK)); + + remoteInput = RemoteInput.getResultsFromIntent(intent); + if (remoteInput != null) { + String inputString = remoteInput.getCharSequence(INLINE_REPLY).toString(); + Log.d(LOG_TAG, "response: " + inputString); + originalExtras.putString(INLINE_REPLY, inputString); + } + + PushPlugin.sendExtras(originalExtras); + } + return remoteInput == null; + } + + /** + * Forces the main activity to re-launch if it's unloaded. + */ + private void forceMainActivityReload(boolean startOnBackground) { + PackageManager pm = getPackageManager(); + Intent launchIntent = pm.getLaunchIntentForPackage(getApplicationContext().getPackageName()); + + Bundle extras = getIntent().getExtras(); + if (extras != null) { + Bundle originalExtras = extras.getBundle(PUSH_BUNDLE); + if (originalExtras != null) { + launchIntent.putExtras(originalExtras); + } + launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + launchIntent.addFlags(Intent.FLAG_FROM_BACKGROUND); + launchIntent.putExtra(START_IN_BACKGROUND, startOnBackground); + } + + startActivity(launchIntent); + } + + @Override + protected void onResume() { + super.onResume(); + final NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancelAll(); + } +} diff --git a/StoneIsland/platforms/android/src/com/adobe/phonegap/push/PushInstanceIDListenerService.java b/StoneIsland/platforms/android/src/com/adobe/phonegap/push/PushInstanceIDListenerService.java new file mode 100644 index 00000000..eaa39a48 --- /dev/null +++ b/StoneIsland/platforms/android/src/com/adobe/phonegap/push/PushInstanceIDListenerService.java @@ -0,0 +1,27 @@ +package com.adobe.phonegap.push; + +import android.content.Intent; +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import com.google.android.gms.iid.InstanceID; +import com.google.android.gms.iid.InstanceIDListenerService; + +import org.json.JSONException; + +import java.io.IOException; + +public class PushInstanceIDListenerService extends InstanceIDListenerService implements PushConstants { + public static final String LOG_TAG = "PushPlugin_PushInstanceIDListenerService"; + + @Override + public void onTokenRefresh() { + SharedPreferences sharedPref = getApplicationContext().getSharedPreferences(COM_ADOBE_PHONEGAP_PUSH, Context.MODE_PRIVATE); + String senderID = sharedPref.getString(SENDER_ID, ""); + if (!"".equals(senderID)) { + Intent intent = new Intent(this, RegistrationIntentService.class); + startService(intent); + } + } +} diff --git a/StoneIsland/platforms/android/src/com/adobe/phonegap/push/PushPlugin.java b/StoneIsland/platforms/android/src/com/adobe/phonegap/push/PushPlugin.java new file mode 100644 index 00000000..f6faaa2b --- /dev/null +++ b/StoneIsland/platforms/android/src/com/adobe/phonegap/push/PushPlugin.java @@ -0,0 +1,458 @@ +package com.adobe.phonegap.push; + +import android.app.NotificationManager; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.util.Log; + +import com.google.android.gms.gcm.GcmPubSub; +import com.google.android.gms.iid.InstanceID; + +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.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.ArrayList; +import java.util.List; + +import me.leolin.shortcutbadger.ShortcutBadger; + +public class PushPlugin extends CordovaPlugin implements PushConstants { + + public static final String LOG_TAG = "PushPlugin"; + + private static CallbackContext pushContext; + private static CordovaWebView gWebView; + private static List<Bundle> gCachedExtras = Collections.synchronizedList(new ArrayList<Bundle>()); + private static boolean gForeground = false; + + private static String registration_id = ""; + + /** + * Gets the application context from cordova's main activity. + * @return the application context + */ + private Context getApplicationContext() { + return this.cordova.getActivity().getApplicationContext(); + } + + @Override + public boolean execute(final String action, final JSONArray data, final CallbackContext callbackContext) { + Log.v(LOG_TAG, "execute: action=" + action); + gWebView = this.webView; + + if (INITIALIZE.equals(action)) { + cordova.getThreadPool().execute(new Runnable() { + public void run() { + pushContext = callbackContext; + JSONObject jo = null; + + Log.v(LOG_TAG, "execute: data=" + data.toString()); + SharedPreferences sharedPref = getApplicationContext().getSharedPreferences(COM_ADOBE_PHONEGAP_PUSH, Context.MODE_PRIVATE); + String senderID = null; + + try { + jo = data.getJSONObject(0).getJSONObject(ANDROID); + + Log.v(LOG_TAG, "execute: jo=" + jo.toString()); + + senderID = jo.getString(SENDER_ID); + + Log.v(LOG_TAG, "execute: senderID=" + senderID); + + String savedSenderID = sharedPref.getString(SENDER_ID, ""); + registration_id = InstanceID.getInstance(getApplicationContext()).getToken(senderID, GCM); + + if (!"".equals(registration_id)) { + JSONObject json = new JSONObject().put(REGISTRATION_ID, registration_id); + + Log.v(LOG_TAG, "onRegistered: " + json.toString()); + + JSONArray topics = jo.optJSONArray(TOPICS); + subscribeToTopics(topics, registration_id); + + PushPlugin.sendEvent( json ); + } else { + callbackContext.error("Empty registration ID received from GCM"); + return; + } + } catch (JSONException e) { + Log.e(LOG_TAG, "execute: Got JSON Exception " + e.getMessage()); + callbackContext.error(e.getMessage()); + } catch (IOException e) { + Log.e(LOG_TAG, "execute: Got JSON Exception " + e.getMessage()); + callbackContext.error(e.getMessage()); + } + + if (jo != null) { + SharedPreferences.Editor editor = sharedPref.edit(); + try { + editor.putString(ICON, jo.getString(ICON)); + } catch (JSONException e) { + Log.d(LOG_TAG, "no icon option"); + } + try { + editor.putString(ICON_COLOR, jo.getString(ICON_COLOR)); + } catch (JSONException e) { + Log.d(LOG_TAG, "no iconColor option"); + } + + boolean clearBadge = jo.optBoolean(CLEAR_BADGE, false); + if (clearBadge) { + setApplicationIconBadgeNumber(getApplicationContext(), 0); + } + + editor.putBoolean(SOUND, jo.optBoolean(SOUND, true)); + editor.putBoolean(VIBRATE, jo.optBoolean(VIBRATE, true)); + editor.putBoolean(CLEAR_BADGE, clearBadge); + editor.putBoolean(CLEAR_NOTIFICATIONS, jo.optBoolean(CLEAR_NOTIFICATIONS, true)); + editor.putBoolean(FORCE_SHOW, jo.optBoolean(FORCE_SHOW, false)); + editor.putString(SENDER_ID, senderID); + editor.commit(); + + } + + if (!gCachedExtras.isEmpty()) { + Log.v(LOG_TAG, "sending cached extras"); + synchronized(gCachedExtras) { + Iterator<Bundle> gCachedExtrasIterator = gCachedExtras.iterator(); + while (gCachedExtrasIterator.hasNext()) { + sendExtras(gCachedExtrasIterator.next()); + } + } + gCachedExtras.clear(); + } + } + }); + } else if (UNREGISTER.equals(action)) { + cordova.getThreadPool().execute(new Runnable() { + public void run() { + try { + SharedPreferences sharedPref = getApplicationContext().getSharedPreferences(COM_ADOBE_PHONEGAP_PUSH, Context.MODE_PRIVATE); + JSONArray topics = data.optJSONArray(0); + if (topics != null && !"".equals(registration_id)) { + unsubscribeFromTopics(topics, registration_id); + } else { + InstanceID.getInstance(getApplicationContext()).deleteInstanceID(); + Log.v(LOG_TAG, "UNREGISTER"); + + // Remove shared prefs + SharedPreferences.Editor editor = sharedPref.edit(); + editor.remove(SOUND); + editor.remove(VIBRATE); + editor.remove(CLEAR_BADGE); + editor.remove(CLEAR_NOTIFICATIONS); + editor.remove(FORCE_SHOW); + editor.remove(SENDER_ID); + editor.commit(); + } + + callbackContext.success(); + } catch (IOException e) { + Log.e(LOG_TAG, "execute: Got JSON Exception " + e.getMessage()); + callbackContext.error(e.getMessage()); + } + } + }); + } else if (FINISH.equals(action)) { + callbackContext.success(); + } else if (HAS_PERMISSION.equals(action)) { + cordova.getThreadPool().execute(new Runnable() { + public void run() { + JSONObject jo = new JSONObject(); + try { + jo.put("isEnabled", PermissionUtils.hasPermission(getApplicationContext(), "OP_POST_NOTIFICATION")); + PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, jo); + pluginResult.setKeepCallback(true); + callbackContext.sendPluginResult(pluginResult); + } catch (UnknownError e) { + callbackContext.error(e.getMessage()); + } catch (JSONException e) { + callbackContext.error(e.getMessage()); + } + } + }); + } else if (SET_APPLICATION_ICON_BADGE_NUMBER.equals(action)) { + cordova.getThreadPool().execute(new Runnable() { + public void run() { + Log.v(LOG_TAG, "setApplicationIconBadgeNumber: data=" + data.toString()); + try { + setApplicationIconBadgeNumber(getApplicationContext(), data.getJSONObject(0).getInt(BADGE)); + } catch (JSONException e) { + callbackContext.error(e.getMessage()); + } + callbackContext.success(); + } + }); + } else if (CLEAR_ALL_NOTIFICATIONS.equals(action)) { + cordova.getThreadPool().execute(new Runnable() { + public void run() { + Log.v(LOG_TAG, "clearAllNotifications"); + clearAllNotifications(); + callbackContext.success(); + } + }); + } else if (SUBSCRIBE.equals(action)){ + // Subscribing for a topic + cordova.getThreadPool().execute(new Runnable() { + public void run() { + try { + String topic = data.getString(0); + subscribeToTopic(topic, registration_id); + callbackContext.success(); + } catch (JSONException e) { + callbackContext.error(e.getMessage()); + } catch (IOException e) { + callbackContext.error(e.getMessage()); + } + } + }); + } else if (UNSUBSCRIBE.equals(action)){ + // un-subscribing for a topic + cordova.getThreadPool().execute(new Runnable(){ + public void run() { + try { + String topic = data.getString(0); + unsubscribeFromTopic(topic, registration_id); + callbackContext.success(); + } catch (JSONException e) { + callbackContext.error(e.getMessage()); + } catch (IOException e) { + callbackContext.error(e.getMessage()); + } + } + }); + } else { + Log.e(LOG_TAG, "Invalid action : " + action); + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.INVALID_ACTION)); + return false; + } + + return true; + } + + public static void sendEvent(JSONObject _json) { + PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, _json); + pluginResult.setKeepCallback(true); + if (pushContext != null) { + pushContext.sendPluginResult(pluginResult); + } + } + + public static void sendError(String message) { + PluginResult pluginResult = new PluginResult(PluginResult.Status.ERROR, message); + pluginResult.setKeepCallback(true); + if (pushContext != null) { + pushContext.sendPluginResult(pluginResult); + } + } + + /* + * Sends the pushbundle extras to the client application. + * If the client application isn't currently active, it is cached for later processing. + */ + public static void sendExtras(Bundle extras) { + if (extras != null) { + if (gWebView != null) { + sendEvent(convertBundleToJson(extras)); + } else { + Log.v(LOG_TAG, "sendExtras: caching extras to send at a later time."); + gCachedExtras.add(extras); + } + } + } + + public static void setApplicationIconBadgeNumber(Context context, int badgeCount) { + if (badgeCount > 0) { + ShortcutBadger.applyCount(context, badgeCount); + } else { + ShortcutBadger.removeCount(context); + } + } + + @Override + public void initialize(CordovaInterface cordova, CordovaWebView webView) { + super.initialize(cordova, webView); + gForeground = true; + } + + @Override + public void onPause(boolean multitasking) { + super.onPause(multitasking); + gForeground = false; + + SharedPreferences prefs = getApplicationContext().getSharedPreferences(COM_ADOBE_PHONEGAP_PUSH, Context.MODE_PRIVATE); + if (prefs.getBoolean(CLEAR_NOTIFICATIONS, true)) { + clearAllNotifications(); + } + } + + @Override + public void onResume(boolean multitasking) { + super.onResume(multitasking); + gForeground = true; + } + + @Override + public void onDestroy() { + super.onDestroy(); + gForeground = false; + gWebView = null; + } + + private void clearAllNotifications() { + final NotificationManager notificationManager = (NotificationManager) cordova.getActivity().getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancelAll(); + } + + /** + * Transform `topic name` to `topic path` + * Normally, the `topic` inputed from end-user is `topic name` only. + * We should convert them to GCM `topic path` + * Example: + * when topic name = 'my-topic' + * then topic path = '/topics/my-topic' + * + * @param String topic The topic name + * @return The topic path + */ + private String getTopicPath(String topic) + { + return "/topics/" + topic; + } + + private void subscribeToTopics(JSONArray topics, String registrationToken) throws IOException { + if (topics != null) { + String topic = null; + for (int i=0; i<topics.length(); i++) { + topic = topics.optString(i, null); + subscribeToTopic(topic, registrationToken); + } + } + } + + private void subscribeToTopic(String topic, String registrationToken) throws IOException + { + try { + if (topic != null) { + Log.d(LOG_TAG, "Subscribing to topic: " + topic); + GcmPubSub.getInstance(getApplicationContext()).subscribe(registrationToken, getTopicPath(topic), null); + } + } catch (IOException e) { + Log.e(LOG_TAG, "Failed to subscribe to topic: " + topic, e); + throw e; + } + } + + private void unsubscribeFromTopics(JSONArray topics, String registrationToken) { + if (topics != null) { + String topic = null; + for (int i=0; i<topics.length(); i++) { + try { + topic = topics.optString(i, null); + if (topic != null) { + Log.d(LOG_TAG, "Unsubscribing to topic: " + topic); + GcmPubSub.getInstance(getApplicationContext()).unsubscribe(registrationToken, getTopicPath(topic)); + } + } catch (IOException e) { + Log.e(LOG_TAG, "Failed to unsubscribe to topic: " + topic, e); + } + } + } + } + + private void unsubscribeFromTopic(String topic, String registrationToken) throws IOException + { + try { + if (topic != null) { + Log.d(LOG_TAG, "Unsubscribing to topic: " + topic); + GcmPubSub.getInstance(getApplicationContext()).unsubscribe(registrationToken, getTopicPath(topic)); + } + } catch (IOException e) { + Log.e(LOG_TAG, "Failed to unsubscribe to topic: " + topic, e); + throw e; + } + } + + /* + * serializes a bundle to JSON. + */ + private static JSONObject convertBundleToJson(Bundle extras) { + Log.d(LOG_TAG, "convert extras to json"); + try { + JSONObject json = new JSONObject(); + JSONObject additionalData = new JSONObject(); + + // Add any keys that need to be in top level json to this set + HashSet<String> jsonKeySet = new HashSet(); + Collections.addAll(jsonKeySet, TITLE,MESSAGE,COUNT,SOUND,IMAGE); + + Iterator<String> it = extras.keySet().iterator(); + while (it.hasNext()) { + String key = it.next(); + Object value = extras.get(key); + + Log.d(LOG_TAG, "key = " + key); + + if (jsonKeySet.contains(key)) { + json.put(key, value); + } + else if (key.equals(COLDSTART)) { + additionalData.put(key, extras.getBoolean(COLDSTART)); + } + else if (key.equals(FOREGROUND)) { + additionalData.put(key, extras.getBoolean(FOREGROUND)); + } + else if ( value instanceof String ) { + String strValue = (String)value; + try { + // Try to figure out if the value is another JSON object + if (strValue.startsWith("{")) { + additionalData.put(key, new JSONObject(strValue)); + } + // Try to figure out if the value is another JSON array + else if (strValue.startsWith("[")) { + additionalData.put(key, new JSONArray(strValue)); + } + else { + additionalData.put(key, value); + } + } catch (Exception e) { + additionalData.put(key, value); + } + } + } // while + + json.put(ADDITIONAL_DATA, additionalData); + Log.v(LOG_TAG, "extrasToJSON: " + json.toString()); + + return json; + } + catch( JSONException e) { + Log.e(LOG_TAG, "extrasToJSON: JSON exception"); + } + return null; + } + + public static boolean isInForeground() { + return gForeground; + } + + public static boolean isActive() { + return gWebView != null; + } + + protected static void setRegistrationID(String token) { + registration_id = token; + } +} diff --git a/StoneIsland/platforms/android/src/com/adobe/phonegap/push/RegistrationIntentService.java b/StoneIsland/platforms/android/src/com/adobe/phonegap/push/RegistrationIntentService.java new file mode 100644 index 00000000..b181e88e --- /dev/null +++ b/StoneIsland/platforms/android/src/com/adobe/phonegap/push/RegistrationIntentService.java @@ -0,0 +1,38 @@ +package com.adobe.phonegap.push; + +import android.content.Context; + +import android.app.IntentService; +import android.content.Intent; +import android.content.SharedPreferences; +import android.util.Log; + +import com.google.android.gms.gcm.GoogleCloudMessaging; +import com.google.android.gms.iid.InstanceID; + +import java.io.IOException; + +public class RegistrationIntentService extends IntentService implements PushConstants { + public static final String LOG_TAG = "PushPlugin_RegistrationIntentService"; + + public RegistrationIntentService() { + super(LOG_TAG); + } + + @Override + protected void onHandleIntent(Intent intent) { + SharedPreferences sharedPreferences = getApplicationContext().getSharedPreferences(COM_ADOBE_PHONEGAP_PUSH, Context.MODE_PRIVATE); + + try { + InstanceID instanceID = InstanceID.getInstance(this); + String senderID = sharedPreferences.getString(SENDER_ID, ""); + String token = instanceID.getToken(senderID, + GoogleCloudMessaging.INSTANCE_ID_SCOPE, null); + PushPlugin.setRegistrationID(token); + Log.i(LOG_TAG, "new GCM Registration Token: " + token); + + } catch (Exception e) { + Log.d(LOG_TAG, "Failed to complete token refresh", e); + } + } +} |
