diff --git a/.gitignore b/.gitignore index 603dfd4..79bb553 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ build/ build.log .DS_Store android/build.properties -parse.jar \ No newline at end of file +parse.jar +cloudcode/config/local.json \ No newline at end of file diff --git a/android/README.md b/android/README.md index 1947e7f..6c3ff7c 100644 --- a/android/README.md +++ b/android/README.md @@ -84,12 +84,45 @@ Get the object id of the installation Parse.getObjectId(); ``` +## Parse CloudCode + +Android by design has no deviceToken like iOS has. Everytime you install an app on Android the deviceToken will change. This means that when a user re-installs an app, a duplicate Parse installation will be registered and the user will get 2 push notifications if no measures have been taken. +See for reference this [StackOverflow](http://stackoverflow.com/questions/2785485/is-there-a-unique-android-device-id) thread. + +This can be easily overcome since version 0.9 of this module using the strategy found on [Parse questions](https://www.parse.com/questions/check-for-duplicate-installations-of-same-user-on-re-installation-of-app). + +### Deploy Parse Cloudcode + +First install the CLI tool: +``` + curl -s https://www.parse.com/downloads/cloud_code/installer.sh | sudo /bin/bash +``` +Go the Cloudcode directory in this repo and issue the following commands: +``` + echo "{}" > config/local.json + parse add --local + parse default "your parse app name" +``` + +If you've set your application as default you can now deploy: +``` + parse deploy +``` + +This will create your CloudCode application which resolves the duplicate Android installs by inspecting the AndroidID. + +Checkout the [Parse Manual](https://www.parse.com/docs/js/guide#cloud-code) for further information. ## Known Issues -* None +* The current implementation does __NOT__ work in combination with the [Facebook module](https://github.com/appcelerator-modules/ti.facebook) provided by [Appcelerator](https://github.com/appcelerator). The Facebook module has a dependency onto the Boltz framework version 1.1.2, whereas Parse Android SDK 1.9.4 has a dependency onto version 1.2.0. ## Changelog +**[v0.9](https://github.com/timanrebel/Parse/releases/tag/0.9)** +- Upgrade to latest Parse SDK version 1.9.4 +- Add AndroidID to installation registration to be able to detect duplicate installs +- Add optional Parse CloudCode installation + **[v0.8](https://github.com/timanrebel/Parse/releases/tag/0.8)** - Resume the app on notification click if it was in background. diff --git a/android/dist/eu.rebelcorp.parse-android-0.8.zip b/android/dist/eu.rebelcorp.parse-android-0.8.zip index 5034ffd..f85c987 100644 Binary files a/android/dist/eu.rebelcorp.parse-android-0.8.zip and b/android/dist/eu.rebelcorp.parse-android-0.8.zip differ diff --git a/android/dist/eu.rebelcorp.parse-android-0.9.zip b/android/dist/eu.rebelcorp.parse-android-0.9.zip new file mode 100644 index 0000000..c2f0c4b Binary files /dev/null and b/android/dist/eu.rebelcorp.parse-android-0.9.zip differ diff --git a/android/lib/Parse-1.7.1.jar b/android/lib/Parse-1.7.1.jar deleted file mode 100644 index 59fce33..0000000 Binary files a/android/lib/Parse-1.7.1.jar and /dev/null differ diff --git a/android/lib/Parse-1.9.4.jar b/android/lib/Parse-1.9.4.jar new file mode 100644 index 0000000..8d1bc70 Binary files /dev/null and b/android/lib/Parse-1.9.4.jar differ diff --git a/android/lib/bolts-android-1.1.3.jar b/android/lib/bolts-android-1.1.3.jar deleted file mode 100644 index 791a07e..0000000 Binary files a/android/lib/bolts-android-1.1.3.jar and /dev/null differ diff --git a/android/lib/bolts-android-1.2.0.jar b/android/lib/bolts-android-1.2.0.jar new file mode 100644 index 0000000..f5e6d6b Binary files /dev/null and b/android/lib/bolts-android-1.2.0.jar differ diff --git a/android/libs/armeabi-v7a/libeu.rebelcorp.parse.so b/android/libs/armeabi-v7a/libeu.rebelcorp.parse.so index 476f0c8..24c7d36 100644 Binary files a/android/libs/armeabi-v7a/libeu.rebelcorp.parse.so and b/android/libs/armeabi-v7a/libeu.rebelcorp.parse.so differ diff --git a/android/libs/armeabi/libeu.rebelcorp.parse.so b/android/libs/armeabi/libeu.rebelcorp.parse.so index a50f183..9179eef 100644 Binary files a/android/libs/armeabi/libeu.rebelcorp.parse.so and b/android/libs/armeabi/libeu.rebelcorp.parse.so differ diff --git a/android/libs/x86/libeu.rebelcorp.parse.so b/android/libs/x86/libeu.rebelcorp.parse.so index 40b28cd..0efb38d 100644 Binary files a/android/libs/x86/libeu.rebelcorp.parse.so and b/android/libs/x86/libeu.rebelcorp.parse.so differ diff --git a/android/manifest b/android/manifest index 5dd2b60..8e6478f 100644 --- a/android/manifest +++ b/android/manifest @@ -2,7 +2,7 @@ # this is your module manifest and used by Titanium # during compilation, packaging, distribution, etc. # -version: 0.8 +version: 0.9 apiversion: 2 description: Titanium module wrapping the Parse Android SDK. author: Timan Rebel diff --git a/android/src/eu/rebelcorp/parse/ParseModule.java b/android/src/eu/rebelcorp/parse/ParseModule.java index f2416a4..9f99e5c 100644 --- a/android/src/eu/rebelcorp/parse/ParseModule.java +++ b/android/src/eu/rebelcorp/parse/ParseModule.java @@ -16,6 +16,7 @@ import android.content.Context; import android.app.Activity; +import android.provider.Settings.Secure; import com.parse.Parse; import com.parse.ParsePush; @@ -41,8 +42,12 @@ public class ParseModule extends KrollModule public static String PROPERTY_APP_ID = "Parse_AppId"; public static String PROPERTY_CLIENT_KEY = "Parse_ClientKey"; + public static final int STATE_RUNNING = 1; + public static final int STATE_STOPPED = 2; + public static final int STATE_DESTROYED = 3; + /* Control the state of the activity */ - private boolean isModuleRunning; + private int state = STATE_DESTROYED; // You can define constants with @Kroll.constant, for example: // @Kroll.constant public static final String EXTERNAL_NAME = value; @@ -51,7 +56,6 @@ public ParseModule() { super(); module = this; - setIsModuleRunning(true); } @Kroll.onAppCreate @@ -61,7 +65,6 @@ public static void onAppCreate(TiApplication app) String clientKey = TiApplication.getInstance().getAppProperties().getString(ParseModule.PROPERTY_CLIENT_KEY, ""); Log.d(TAG, "Initializing with: " + appId + ", " + clientKey + ";"); - Parse.initialize(TiApplication.getInstance(), appId, clientKey); } @@ -69,42 +72,42 @@ public static void onAppCreate(TiApplication app) public void onStart(Activity activity) { super.onStart(activity); - setIsModuleRunning(true); + setState(STATE_RUNNING); } public void onResume(Activity activity) { super.onResume(activity); - setIsModuleRunning(true); + setState(STATE_RUNNING); } public void onPause(Activity activity) { super.onPause(activity); - setIsModuleRunning(false); + setState(STATE_STOPPED); } public void onStop(Activity activity) { super.onStop(activity); - setIsModuleRunning(false); + setState(STATE_STOPPED); } public void onDestroy(Activity activity) { super.onDestroy(activity); - setIsModuleRunning(false); + setState(STATE_DESTROYED); } - private void setIsModuleRunning(boolean isModuleRunning) + private void setState(int state) { - this.isModuleRunning = isModuleRunning; + this.state = state; } /* An accessor from the outside */ - public boolean isModuleRunning() + public int getState() { - return isModuleRunning; + return state; } /* Get an instance of that module*/ @@ -118,7 +121,9 @@ public void start() { // Track Push opens ParseAnalytics.trackAppOpened(TiApplication.getAppRootOrCurrentActivity().getIntent()); + setState(STATE_RUNNING); + ParseInstallation.getCurrentInstallation().put("androidId", getAndroidId()); ParseInstallation.getCurrentInstallation().saveInBackground(new SaveCallback() { public void done(ParseException e) { if (e != null) { @@ -171,4 +176,9 @@ public String getCurrentInstallationId() { public String getObjectId() { return ParseInstallation.getCurrentInstallation().getObjectId(); } + + protected String getAndroidId() { + Context context = TiApplication.getInstance().getApplicationContext(); + return Secure.getString(context.getContentResolver(), Secure.ANDROID_ID); + } } diff --git a/android/src/eu/rebelcorp/parse/ParseModuleBroadcastReceiver.java b/android/src/eu/rebelcorp/parse/ParseModuleBroadcastReceiver.java index 2ae7b03..9a157ba 100644 --- a/android/src/eu/rebelcorp/parse/ParseModuleBroadcastReceiver.java +++ b/android/src/eu/rebelcorp/parse/ParseModuleBroadcastReceiver.java @@ -28,70 +28,67 @@ public class ParseModuleBroadcastReceiver extends ParsePushBroadcastReceiver { @Override public void onPushOpen(Context context, Intent intent) { - Log.d("onPushOpen", "Clicked"); + Intent i = context.getPackageManager().getLaunchIntentForPackage(context.getApplicationContext().getPackageName()); - if (ParseModule.getInstance() != null && ParseModule.getInstance().isModuleRunning()) { - Log.d("onPushOpen", "App is running in foreground"); + /* Check if the app is running or in background. If not, just start the app and add the + * notification as Extra */ + if (ParseModule.getInstance() == null || ParseModule.getInstance().getState() == ParseModule.STATE_DESTROYED) { + Log.d("onPushOpen", "App was killed; resume the app without triggering 'notificationopen'"); + i.putExtras(intent.getExtras()); + context.startActivity(i); return; } - Log.d("onPushOpen", "App is not running or is in background. Now resume the app."); - Intent i = context.getPackageManager().getLaunchIntentForPackage(context.getApplicationContext().getPackageName()); - i.putExtras(intent.getExtras()); - context.startActivity(i); - - /* Now, the module should be opened */ - if (ParseModule.getInstance() != null) { - try { - Log.d("onPushOpen", "Open a notification"); - KrollDict data = new KrollDict(new JSONObject(intent.getExtras().getString("com.parse.Data"))); - ParseModule.getInstance().fireEvent("notificationopen", data); - } catch (Exception e) { - Log.d("JSON Failure", e.getMessage()); + /* Otherwise, just resume the app if necessary, and trigger the event */ + try { + KrollDict data = new KrollDict(new JSONObject(intent.getExtras().getString("com.parse.Data"))); + + if (ParseModule.getInstance().getState() != ParseModule.STATE_RUNNING) { + Log.d("onPushOpen", "App was in background; resume the app and trigger 'notificationopen'"); + context.startActivity(i); + } else { + Log.d("onPushOpen", "App is running in foreground; trigger 'notificationopen'"); } + + ParseModule.getInstance().fireEvent("notificationopen", data); + } catch (Exception e) { + Log.d("onPushOpen", e.getMessage()); } } - + @Override - public void onReceive(Context context, Intent intent) { + public void onPushReceive(Context context, Intent intent) { try { if (intent == null) { - Log.d("onReceive", "Receiver intent null"); + Log.d("onPushReceive", "Receiver intent null"); + super.onPushReceive(context, intent); return; } if (ParseModule.getInstance() == null) { - Log.d("onReceive", "no instance of ParseModule found"); + Log.d("onPushReceive", "No instance of ParseModule found"); + super.onPushReceive(context, intent); return; } - String action = intent.getAction(); - KrollDict data = new KrollDict(new JSONObject(intent.getExtras().getString("com.parse.Data"))); - - Log.d("onReceive", "got action " + action ); - if (action.equals("com.parse.push.intent.RECEIVE")) { - /* The notification is received by the device */ - Log.d("onReceive", "New notification received"); + /* The notification is received by the device */ + if (ParseModule.getInstance().getState() == ParseModule.STATE_RUNNING) { + Log.d("onPushReceive", "App is in foreground; trigger event 'notificationreceive'"); - if (ParseModule.getInstance().isModuleRunning()) { + try { + KrollDict data = new KrollDict(new JSONObject(intent.getExtras().getString("com.parse.Data"))); ParseModule.getInstance().fireEvent("notificationreceive", data); - } else { - Log.d("onReceive", "App is in background, the event won't be triggered"); - } - } else if (action.equals("com.parse.push.intent.OPEN")) { - /* The user has clicked on the notification */ - Log.d("onReceive", "Notification opened"); - - if (ParseModule.getInstance().isModuleRunning()) { - ParseModule.getInstance().fireEvent("notificationopen", data); - } else { - Log.d("onReceive", "App is in background, the event will be triggered later."); + } catch (Exception e) { + Log.d("onPushReceive", e.getMessage()); } + } else { + Log.d("onPushReceive", "App is in background; 'notificationreceive' won't be triggered"); } + + super.onPushReceive(context, intent); } catch (Exception e) { Log.e("Push", "Exception: " + e.toString()); } - super.onReceive(context, intent); } } diff --git a/cloudcode/cloud/main.js b/cloudcode/cloud/main.js new file mode 100644 index 0000000..280c159 --- /dev/null +++ b/cloudcode/cloud/main.js @@ -0,0 +1,36 @@ +// Parse CloudCode +Parse.Cloud.beforeSave(Parse.Installation, function(request, response) { + Parse.Cloud.useMasterKey(); + + var androidId = request.object.get("androidId"); + if (androidId == null || androidId == "") { + console.warn("No androidId found, exit"); + response.success(); + } + + var query = new Parse.Query(Parse.Installation); + query.equalTo("deviceType", "android"); + query.equalTo("androidId", androidId); + query.addAscending("createdAt"); + query.find().then(function(results) { + for (var i = 0; i < results.length; ++i) { + if (results[i].get("installationId") != request.object.get("installationId")) { + console.warn("App id " + results[i].get("installationId") + ", delete!"); + results[i].destroy().then(function() { + console.warn("Delete success"); + }, + function() { + console.warn("Delete error"); + } + ); + } else { + console.warn("Current App id " + results[i].get("installationId") + ", dont delete"); + } + } + response.success(); + }, + function(error) { + response.error("Can't find Installation objects"); + } + ); +}); \ No newline at end of file diff --git a/cloudcode/config/global.json b/cloudcode/config/global.json new file mode 100644 index 0000000..d29f691 --- /dev/null +++ b/cloudcode/config/global.json @@ -0,0 +1,11 @@ +{ + "global": { + "parseVersion": "1.5.0" + }, + "applications": { + "dummy": { + "applicationId": "dummy", + "masterKey": "dummy" + } + } +} \ No newline at end of file diff --git a/cloudcode/public/index.html b/cloudcode/public/index.html new file mode 100644 index 0000000..8453fae --- /dev/null +++ b/cloudcode/public/index.html @@ -0,0 +1,21 @@ + + +
+To get started, edit this file at public/index.html and start adding static content.
+If you want something a bit more dynamic, delete this file and check out our hosting docs.
+