Skip to content

Commit

Permalink
Merge pull request #25 from jvandijk/feature/duplicate-android-installs
Browse files Browse the repository at this point in the history
Feature: Prevent duplicate android installs
  • Loading branch information
timanrebel committed Jul 28, 2015
2 parents 558cd36 + dbfb4d8 commit 0ee8b22
Show file tree
Hide file tree
Showing 17 changed files with 164 additions and 55 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ build/
build.log
.DS_Store
android/build.properties
parse.jar
parse.jar
cloudcode/config/local.json
35 changes: 34 additions & 1 deletion android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Binary file modified android/dist/eu.rebelcorp.parse-android-0.8.zip
Binary file not shown.
Binary file added android/dist/eu.rebelcorp.parse-android-0.9.zip
Binary file not shown.
Binary file removed android/lib/Parse-1.7.1.jar
Binary file not shown.
Binary file added android/lib/Parse-1.9.4.jar
Binary file not shown.
Binary file removed android/lib/bolts-android-1.1.3.jar
Binary file not shown.
Binary file added android/lib/bolts-android-1.2.0.jar
Binary file not shown.
Binary file modified android/libs/armeabi-v7a/libeu.rebelcorp.parse.so
Binary file not shown.
Binary file modified android/libs/armeabi/libeu.rebelcorp.parse.so
Binary file not shown.
Binary file modified android/libs/x86/libeu.rebelcorp.parse.so
Binary file not shown.
2 changes: 1 addition & 1 deletion android/manifest
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 22 additions & 12 deletions android/src/eu/rebelcorp/parse/ParseModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -51,7 +56,6 @@ public ParseModule()
{
super();
module = this;
setIsModuleRunning(true);
}

@Kroll.onAppCreate
Expand All @@ -61,50 +65,49 @@ 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);
}

/* Get control over the module's state */
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*/
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
}
}
77 changes: 37 additions & 40 deletions android/src/eu/rebelcorp/parse/ParseModuleBroadcastReceiver.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
36 changes: 36 additions & 0 deletions cloudcode/cloud/main.js
Original file line number Diff line number Diff line change
@@ -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");
}
);
});
11 changes: 11 additions & 0 deletions cloudcode/config/global.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"global": {
"parseVersion": "1.5.0"
},
"applications": {
"dummy": {
"applicationId": "dummy",
"masterKey": "dummy"
}
}
}
21 changes: 21 additions & 0 deletions cloudcode/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

<html>
<head>
<title>My ParseApp site</title>
<style>
body { font-family: Helvetica, Arial, sans-serif; }
div { width: 800px; height: 400px; margin: 40px auto; padding: 20px; border: 2px solid #5298fc; }
h1 { font-size: 30px; margin: 0; }
p { margin: 40px 0; }
em { font-family: monospace; }
a { color: #5298fc; text-decoration: none; }
</style>
</head>
<body>
<div>
<h1>Congratulations! You're already hosting with Parse.</h1>
<p>To get started, edit this file at <em>public/index.html</em> and start adding static content.</p>
<p>If you want something a bit more dynamic, delete this file and check out <a href="https://parse.com/docs/hosting_guide#webapp">our hosting docs</a>.</p>
</div>
</body>
</html>

0 comments on commit 0ee8b22

Please sign in to comment.