I've been trying for days now to get a working Remote Hybrid Android app to build for Android using the current (4.1) build of the Salesforce Mobile SDK. I've made some progress (I think), but remain stuck. This is a log of my progress so far.
Using forcedroid
to create an app skeleton currently results in errors, as has been documented in the Salesforce Mobile SDK Google Group.
nvm use v0.12.2
forcedroid create --apptype=hybrid_remote --appname=sdktest --targetdir=. \
--packagename=org.jclark.sdktest --startpage=/apex/BasicVFPage
eventually leads to:
Fetching plugin "phonegap-plugin-push" via npm
Installing "phonegap-plugin-push" for android
Failed to install 'phonegap-plugin-push':Error: Variable(s) missing: SENDER_ID
at /Users/jclark/.nvm/versions/node/v0.12.2/lib/node_modules/cordova/node_modules/cordova-lib/src/plugman/install.js:346:23
at _fulfilled (/Users/jclark/.nvm/versions/node/v0.12.2/lib/node_modules/cordova/node_modules/q/q.js:787:54)
at self.promiseDispatch.done (/Users/jclark/.nvm/versions/node/v0.12.2/lib/node_modules/cordova/node_modules/q/q.js:816:30)
at Promise.promise.promiseDispatch (/Users/jclark/.nvm/versions/node/v0.12.2/lib/node_modules/cordova/node_modules/q/q.js:749:13)
at /Users/jclark/.nvm/versions/node/v0.12.2/lib/node_modules/cordova/node_modules/q/q.js:557:44
at flush (/Users/jclark/.nvm/versions/node/v0.12.2/lib/node_modules/cordova/node_modules/q/q.js:108:17)
at process._tickCallback (node.js:355:11)
Failed to install 'com.salesforce':Error: Variable(s) missing: SENDER_ID
at /Users/jclark/.nvm/versions/node/v0.12.2/lib/node_modules/cordova/node_modules/cordova-lib/src/plugman/install.js:346:23
at _fulfilled (/Users/jclark/.nvm/versions/node/v0.12.2/lib/node_modules/cordova/node_modules/q/q.js:787:54)
at self.promiseDispatch.done (/Users/jclark/.nvm/versions/node/v0.12.2/lib/node_modules/cordova/node_modules/q/q.js:816:30)
at Promise.promise.promiseDispatch (/Users/jclark/.nvm/versions/node/v0.12.2/lib/node_modules/cordova/node_modules/q/q.js:749:13)
at /Users/jclark/.nvm/versions/node/v0.12.2/lib/node_modules/cordova/node_modules/q/q.js:557:44
at flush (/Users/jclark/.nvm/versions/node/v0.12.2/lib/node_modules/cordova/node_modules/q/q.js:108:17)
at process._tickCallback (node.js:355:11)
Error: Variable(s) missing: SENDER_ID
If you attempt to run the app now (cordova run android
), the app will build and run, however, you'll get a ERR_FILE_NOT_FOUND (file:///android_asset/www/index.html
popup, which I would not expect from a Remote hybrid app.
However, the error text indicates that the push plugin was not installed correctly, and looking at the command output and the source of forcedroid
convinced me that some of the installation steps are being skipped when the plugin install fails. Some folks in the MobileSDK Google Group reported removing the new phonegap-plugin-push
and adding the older salesforce push plugin, but I could not get a working build this way, so I set out to fix the underlying forcedroid
error.
The root issue is that the new push plugin, phonegap-plugin-push
, expects a variable SENDER_ID to be defined, containing the Android Push Notification Client Id; the plugin installation docs show the variable being passed as part of the cordova plugin add
commandline. Unfortunately, the push plugin isn't added with that command. Instead, forcedroid
adds the plugin SalesforceMobileSDK-CordovaPlugin, which lists the push plugin as a dependency in its plugin.xml
. A little experimentation showed that I could add the variable to the dependency declaration in such a way that it would use an ENV variable of the same name. I forked the plugin and made that change, then edited my local forcedroid
(in my case, /Users/jclark/.nvm/versions/node/v0.12.2/forcedroid
to change the plugin add
line:
//shelljs.exec('cordova plugin add https://github.com/forcedotcom/SalesforceMobileSDK-CordovaPlugin#v' + version);
shelljs.exec('cordova plugin add https://github.com/jclark-dot-org/SalesforceMobileSDK-CordovaPlugin');
With this change in place, running forcedroid
with a SENDER_ID ENV variable declared:
SENDER_ID=0000000000 forcedroid create --apptype=hybrid_remote --appname=sdktest --targetdir=. --packagename=org.jclark.sdktest --startpage=/apex/BasicVFPage
Runs without error. Running the app (cordova run android
) will succeed like the first test (app runs, cannot find index.html
), but further experimentation has shown that there's still an issue with the basic app framework... the after-platform-add-android
hook of the Salesforce plugin (plugins/com.salesforce) never runs. This script makes some changes to the AndroidManifest.xml
, such as commenting out the first <application>
tag found - presumably so that it can be replaced by the version in the Salesforce plugin's plugin.xml
file. Adding any plugin will fire this hook, and then running cordova prepare android
will cause AndroidManifest.xml
to be updated. For example:
cordova plugin add cordova-plugin-badge
cordova prepare android
Now an attempt to build the app will fail:
:processDebugManifest
/Users/jclark/sdktest/platforms/android/AndroidManifest.xml:30:9 Error:
Element activity#com.adobe.phonegap.push.PushHandlerActivity at AndroidManifest.xml:30:9 duplicated with element declared at AndroidManifest.xml:6:9
/Users/jclark/sdktest/platforms/android/AndroidManifest.xml:0:0 Error:
Validation failed, exiting
The reason for this is somewhat complicated, but this is what I've determined: when the after-platform-add-android
hook runs, it removes the first <activity>
tag from AndroidManifest.xml
, which the salesforce plugin replaces (see above), however, this changes the order of the tags within the <application>
tag. The activity
which is removed and replaced is the "primary" activity (containing android:name="com.salesforce.androidsdk.phonegap.ui.SalesforceDroidGapActivity"
); It is originally the first child of application
, but after the manifest is updated it becomes the last child. This makes <activity android:exported="true" android:name="com.adobe.phonegap.push.PushHandlerActivity" />
become the first child. Then the cordova prepare
step updates the first activity
element found to set the android:launchMode
explicitly, so the activity becomes <activity android:exported="true" android:launchMode="singleTop" android:name="com.adobe.phonegap.push.PushHandlerActivity" />
. However (and this is where I'm theorizing based on empirical evidence), it looks like the build process is re-writing the manifest by adding missing items; since it expects the version without android:launchMode="singleTop"
, it ends up adding it again. I tried to work around this by adding android:launchMode="singleTop"
to the main activity
in the plugins.xml
file (a change which remains in my fork, since it was in the original activity), but that doesn't stop this from happening. Since I didn't want to fork the push plugin to change the plugin.xml to add the launchMode attrib, the workaround I've found is to manually edit the android.json
file to add it there. Then I remove all of the of the contents of <application>
from the manifest, and run cordova prepare android
to rebuild the manifest.
This moves past the prior error, but fails with:
/Users/jclark/sdktest/platforms/android/build/intermediates/manifests/full/debug/AndroidManifest.xml:45: error: Error: No resource found that matches the given name (at 'icon' with value '@drawable/sf__icon').
I addressed this with one more change to my fork of the SalesforceMobileSDK-CordovaPlugin, by updating the after-platform-add-android
hook to change sf__icon
to icon
.
So at this point, I can set the ENV var SETTING_ID
, run my custom forcedroid
which uses my fork of the SalesforceMobileSDK-CordovaPlugin, add a plugin, run prepare, modify platforms/android/android.json to add android:launchMode=\"singleTop\" to the com.adobe.phonegap.push.PushHandlerActivity
line, and run prepare
once more to get a project that will build. However, when I run it on device, I immediately get "Unfortunately the application has stopped". logcat
shows the following:
E/AndroidRuntime( 5009): FATAL EXCEPTION: main
E/AndroidRuntime( 5009): Process: org.jclark.sdktest, PID: 5009
E/AndroidRuntime( 5009): java.lang.RuntimeException: Unable to instantiate application com.salesforce.androidsdk.phonegap.app.HybridApp: java.lang.ClassNotFoundException: Didn't find class "com.salesforce.androidsdk.phonegap.app.HybridApp" on path: DexPathList[[zip file "/data/app/org.jclark.sdktest-1.apk"],nativeLibraryDirectories=[/data/app-lib/org.jclark.sdktest-1, /vendor/lib, /system/lib]]
E/AndroidRuntime( 5009): at android.app.LoadedApk.makeApplication(LoadedApk.java:516)
E/AndroidRuntime( 5009): at android.app.ActivityThread.handleBindApplication(ActivityThread.java:4327)
E/AndroidRuntime( 5009): at android.app.ActivityThread.access$1500(ActivityThread.java:135)
E/AndroidRuntime( 5009): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1256)
E/AndroidRuntime( 5009): at android.os.Handler.dispatchMessage(Handler.java:102)
E/AndroidRuntime( 5009): at android.os.Looper.loop(Looper.java:136)
E/AndroidRuntime( 5009): at android.app.ActivityThread.main(ActivityThread.java:5027)
E/AndroidRuntime( 5009): at java.lang.reflect.Method.invokeNative(Native Method)
E/AndroidRuntime( 5009): at java.lang.reflect.Method.invoke(Method.java:515)
E/AndroidRuntime( 5009): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:779)
E/AndroidRuntime( 5009): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:595)
E/AndroidRuntime( 5009): at dalvik.system.NativeStart.main(Native Method)
E/AndroidRuntime( 5009): Caused by: java.lang.ClassNotFoundException: Didn't find class "com.salesforce.androidsdk.phonegap.app.HybridApp" on path: DexPathList[[zip file "/data/app/org.jclark.sdktest-1.apk"],nativeLibraryDirectories=[/data/app-lib/org.jclark.sdktest-1, /vendor/lib, /system/lib]]
E/AndroidRuntime( 5009): at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:56)
E/AndroidRuntime( 5009): at java.lang.ClassLoader.loadClass(ClassLoader.java:497)
E/AndroidRuntime( 5009): at java.lang.ClassLoader.loadClass(ClassLoader.java:457)
E/AndroidRuntime( 5009): at android.app.Instrumentation.newApplication(Instrumentation.java:975)
E/AndroidRuntime( 5009): at android.app.LoadedApk.makeApplication(LoadedApk.java:511)
E/AndroidRuntime( 5009): ... 11 more
The key bit of course is Didn't find class "com.salesforce.androidsdk.phonegap.app.HybridApp"
. It appears that the app is being built without all of the SDK components included. At this point I've exhausted my knowledge of the Cordova/Android build process. I tried adding the SDK libs directly to platforms/android/project.properties
, e.g.:
android.library.reference.2=SalesforceSDK
android.library.reference.3=SalesforceHybrid
android.library.reference.4=SmartStore
android.library.reference.5=SmartSync
But that just led to more build errors; I'm thinking that I'm going in the wrong direction there... the plugin is part of the project, so I shouldn't need to add explicit references.
At this point, I'm not sure what to try next. My ideal solution would be a working forcedroid
that can build a working Hybrid Remote android app from scratch, supports push, and continues to work when other plugins are added. Once I have a set of steps to do this, I can work on my original problem of porting an existing SDK 3.x project to SDK 4.1. I will be cross-posting this to the SDK Google Group as well.
Update 3/22: On the Salesforce MobileSDK Google Group thread for this post, SDK Dev Bharath Hariharan pointed me to a recent commit in the unstable
branch of the Cordova Plugin that addresses the first issue (push plugin errors) by pinning the versions of all dependencies. I updated my local forcedroid to use the latest unstable commit:
//shelljs.exec('cordova plugin add https://github.com/forcedotcom/SalesforceMobileSDK-CordovaPlugin#v' + version);
//shelljs.exec('cordova plugin add https://github.com/jclark-dot-org/SalesforceMobileSDK-CordovaPlugin');
shelljs.exec('cordova plugin add https://github.com/forcedotcom/SalesforceMobileSDK-CordovaPlugin#a45425d8e831c9567a0d0bf8b05bc3e1b19b4f23');
This fixes the first problem (push plugin errors). While testing this, I found that for some reason the android post install script wasn't finding shelljs, so I added it locally as suggested in the script error message (npm install [email protected]
); this allowed all post-run scripts to run successfully during the forcedroid
run, and eliminates the need to add another plugin to get a fully updated AndroidManifest.xml
. So at this point, I can run the updated forcedroid
using the latest unstable SDK Cordova plugin, run cordova prepare android
to cause the duplicate activity issue, and manually add the launchmode to the push activity in android.json
and remove the resulting duplicate activity in AndroidManifest to get a build that will install on the device. It still crashes on launch with the Didn't find class "com.salesforce.androidsdk.phonegap.app.HybridApp"
exception.