Due to a bug on our system, we mistakenly stored multiple registration ids per device per user by appending to their profile, rather than replacing after an upgrade and the like.
We can obviously now correct that by handling the canonical ids from the response we get, but for some users (like myself, for instance...) they would get multiple notifications, instead of just one per device.
I've read what can be sent via,
http://developer.android.com/google/gcm/http.html
and
http://developer.android.com/google/gcm/server.html#params
But there doesn't seem to be a perfect solution. I've just checked there, and I could send a 'dry_run' request first, handle the multiple registration ids, replace what needs replacing, remove what needs removing, then send the second (pruned) request.
This certainly could be a solution, but I can't imagine it'd be friendly to our api quotas (though I don't really know). Is there no other property that can be set on the HTTP equest so as to only send to an individual device?
GCM is completely free no matter how big your messaging needs are, and there are no quotas.
(quote from here).
There are no quotas to GCM, so you can try your dry_run approach without fear. The only question is whether dry_run mode actually returns Canonical Registration ID, or simply returns immediately with just some fake message ID.
Using the Canonical Registration ID response is the only way you can use to clean your DB (other than deleting your DB and re-building it from scratch).
There is a small optimization you can make in your cleanup process. If you can fetch the registration IDs from your DB in the order they were inserted (from the oldest to the latest), you will likely get Canonical Registration ID responses for the first registration IDs you try. For each of them you'll know what the current (canonical) registration ID is, and you'll mark it, so you don't send to it during your cleanup process. This will prevent sending duplicate messages for all devices that have up to 2 registration IDs in your DB (and will reduce by one the number of duplicate messages for the other devices).
Related
Basically, I want to cover the next scenario in my App.
I can make group's devices between users. Also, I could add or remove one user from those groups. What would happen if one user changed his smartphone, and I don't want to remove his registration_id in his groups and just swap it with a new one?
I've been reading the whole Google documentation but I didn't find anything. So the only think that I found was use a petition to remove the RegistrationID from all groups and then add again the new registration ID, but I see this really complicated and doesn't make any sense.
Do you know guys if is there the "best" way to do that?
I handle the scenario like registration on the server, what i do is check if user already registered and if registered i check if registration id is different to one stored, if is different I check all groups the user belongs to and call delete with old registration id and add with the new Id to each group, make sure update the database with the new registration id after/before.
I'm integrating gcm on the server side but I've found an unrecoverable issue while managing user groups. Consider the following workflow:
Create user group with notification_key_name = "userid"
GCM replies with the notification_key
Storing the notification_key in the database fails for some reason (even with retries)
Now if the overall process happens again:
Create user group with notification_key_name = "userid";
GCM replies with an error: "notification_key already exists"
As you see, the GCM create group operation is not idempotent. Because of this, this user group will be completely unusable since there's no way to retrieve its notification_key. Also, there's no way to delete a group or to empty it because its notification_key is not known in the second iteration.
Call me crazy, but when I'm developing I usually think about all the error situations in the code path.
Any suggestions about how to deal with the situation explained above?
Thanks
for my first question on StackOverflow I'm gonna ask about Google Cloud Messaging service, and in particular Loopback's implementation.
So, I'm developing an app and started to work on a different branch to introduce Loopback's push notification handling and it's various tools for REST Api. Even if this topic is gonna cover strictly Loopback's way to handle GCM, the question is also related to the original way as is described on Google docs.
So, the main idea behind GCM's kick-off is to check whether the device is registered or not.
This is done by a simple check on a SharedPreferences variable, a name used to store our RegistrationID value.
final LocalInstallation installation = new LocalInstallation(context, adapter);
If this is found, the device has to notify the server, communicating the token.
Else, a registration to GCM has to be done.
Once this is done, the device notifies the server. ( registerInBackground(installation) will eventually call saveInstallation(installation) after retrieving RegistrationId )
if (installation.getDeviceToken() != null) {
saveInstallation(installation);
} else {
registerInBackground(installation);
}
If communication is successful, the device saves RegistrationId using SharedPreferences as described above. (NOTE : getDeviceToken() is Loopback's way to handle via API the value in SharedPreferences)
Let's say this "GCM-Check" is being done every time my MainActivity is being created (so, during the onCreate method).
We also know GCM is sometimes messy, and wants to refresh my app's RegistrationId or some other stuff that, to be honest, is not completely clear to me right now. In short terms, GCM invalidates the token of my app. This causes an error-message when the server send a push-notification using the Token bound to my device-app.
An error similar to
{"multicast_id":0123456789012345678,"success":0,"failure":1,"canonical_ids":0,"results":[{"error":"NotRegistered"}]}
You can see, "failure":1 and "results":[{"error":"NotRegistered"}]
Loopback reacts just as Google docs say, by having the server remove the record of the device linked to the faulty RegistrationId .
Comprehensible.
Back to our device. Launching my app again and loading MainActivity, causes the same "GCM-check" procedure. This time the app can find RegistrationId using SharedPreferences, and can directly notify the server, which creates a record with the given RegistrationId.
No new registration is being handled by the device-app.
You can see the loop in here. Device will have no knowledge of it's token invalidity and will continue to tell the server the same data, which will keep sending information to the wrong registrationId, thus removing it after receiving the related error.
The problem is that the app has to rely on data which is created once and never gets modified. To remove the old data I should send a notification to the device, which is obviously not possible as I can't reach it from GCM. Other solutions possible is notify the user by sending an email or sms, and ask him to click a button for example, but I'd rather have a more "automated" approach to the problem.
A VERY BAD SOLUTION I'VE FOUND
As to my knowledge the only error-info is returned from GCM to the server during a push-notification, I've made a little hack on the device.
The idea is simple: create a POST request to GCM Servers, using the headers my server should use to authenticate. This causes the error to be given to the device itself, which can parse the JSON and notice what happened. From here the app can forge a new registration process, fixing the issue.
What is bad about this? The fact that to authenticate the device as the server, I have to hard-code the ServerKey and distribute it in every app. The ServerKey should be used only on the server, making this solution a very bad idea.
This being said, the idea of simply letting the device know its state using a SharedPreference value is not so great, as it would only tell me if I ever registered at least once, without letting me know my current status.
On other apps I've developed which use GCM just as well, I've solved in different ways, like having a Button to be clicked by the user or reading some specials SMS send by the server, which then enable GoogleCloudMessaging.unregister() at first and eventually GoogleCloudMessaging.register()
So, asking for forgiveness for such a wall-of-text, how have you solved this NotRegistered thing?
Thanks for your effort and time in reading and, hopefully, in answering : )
As an addendum to my comments, since it helped and I have more space here:
Instead of just checking whether or not the token exists inside your SharedPreferences, you should also check to see if the version of your app that token is for matches the version that is currently running the check (that was a suggestion from Google's docs).
If the device version do not match, you should request a valid token (which could actually be the same, but is not guaranteed). Now, you may also want to check for the PACKAGE_REPLACED broadcast (in case you're not incrementing the version in your manifest each time you install for tests, which I'm very guilty of) which, if triggered, should also force you to request a new token.
As for why there's sometimes a change in the token and sometimes not: I couldn't agree more that it's totally sporadic and can't help feeling like there's something going on we don't really know about.
Sometimes the new request after PACKAGE_REPLACED returns the same key; sometimes it doesn't. What happens in between those that makes it weird? God, I would LOVE to know, and I wish I had more info on it. But that's part of why I mentioned trying to catch that broadcast: forcing the check when it happens should ensure that you're always getting a new valid one (in the event that the version check passes when it shouldn't) if it's available.
Hopefully this helps~
REST Server
I created a Rails server that contains :users and associated :comments. It is used as a backend API for an Android client. The exchange format to load and store data at the server is JSON. Here are the relevant migrations.
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name
t.timestamps
end
end
end
...
class CreateComments < ActiveRecord::Migration
def change
create_table :comments do |t|
t.references :users
t.string :subject
t.text :message
t.timestamps
end
end
end
All users have already been imported. Therefore, only read access is configured for the :users resource. Thus, for :comments it should be possible to add new entries. Here are the available routes.
user_comments GET /users/:user_id/comments(.:format) comments#index
POST /users/:user_id/comments(.:format) comments#create
new_user_comment GET /users/:user_id/comments/new(.:format) comments#new
user_comment GET /users/:user_id/comments/:id(.:format) comments#show
users GET /users(.:format) users#index
user GET /users/:id(.:format) users#show
Android client
On the client side I use a Service with AsyncTasks to download, parse and store users into a local SQLite database. A ContentProvider delivers cached users to the UI. The user object downloaded from the server contains the unique id of the users table. This should be useful when a new comment gets created on the client.
Scenario 1: Read comments
Users are displayed in a list view on the Android client.
A user item gets selected.
The list activity creates an Intent which contains the user specific URI, e.g. content://com.example.myapp.provider/users/23.
A user activity displays detail information about the user and associated comments.
Cached comments get loaded via a CursorLoader. (1)
A synchronization process loads comments from the remote server. (2)
Scenario 2: Write comments
A comment can be created from the user activity.
The comment gets stored into the local database. (3)
Stored comments are sychronized with the remote server. (2)
Headache questions
I marked the scenario steps that are associated with the following questions.
How do I create a content URI for the comments being used with a CursorLoader in the user activity? Please mind, I only know the user URI at this point.
Can someone describe how I create a synchronization process? I am not sure if a SyncAdapter works here (never used it). Is the the synchronization process just a Service that on the one hand starts tasks to download, parse and store comments on the client and on the other hand loads, encodes and sends out comments to the server?
How does the content URI for a new comment look like? Is the ContentProvider for comments the same as for users? Is there only one SQLiteOpenHelper in the application?
The main problem I am struggling with is how to design the application? If you know of a better solution on how I should synchronize the users and their associated comments, you are very welcome.
Answers
Answers to question 1. and 3.
I extended the REST model as follows: The JSON hash returned for a comment now includes the id of the associated user. The same id is also included in the JSON hash for the user. Both objects are stored into the local database on the Android device. This allows me request comments for a specific user. I simply pass the server user id as WHERE clause. The content URI for comments is not cascaded as I implied with my question. It is similar to the user content URI:
content://com.example.myapp.provider.commentsprovider/comments
Note, that I changed the authority part of the string. I decided to create separate content provider for users and comments.
One simple architecture is to always update things in the server first, sending posted comments to the server right away and from there pushing a notification through GCM to users that should request the updated comments list. The flow would look like:
When the app is open send GCM registration id to your push notification server (say uniqush-push, or your own server using a gem to handle the GCM logic), this way you can use it to send push notifications to the user telling the app to update the comments from the server
Build your initial cache as you want it
Whenever a user posts a comment, send it to the server and make the server respond with the data for the created comment, so the app can use that and already cache it if it wants, using the returned id and whatever else
On the server, when a comment is posted, loop through all the concerned users and using the GCM registration id send it a push notification, it could be as simple as having just "update_comments": "1"
On the app when the push notification is tapped by the user, update the comments cache with a request to the server
I'm building an app that uses c2dm.
I think that I'm supposed to ask for a registration ID whenever my app's main "intent" starts up. I've been doing this, but it seems that each request results in a new string, so I end up piling up numerous registration IDs in my database for the same device. I presume that only one of them will work. However, I don't want to just delete the old ones because I want my user to be able to receive notifications on more than one device if they own more than one.
How should I handle this?
Never to refresh ID untill your app is installed on your Device
I have implemented C2DM. If you have more than one registration ID for one device then only latest ID will work for sending notification.
When you register your device to google server then it generate a String(As you already know) corresponding to that device ID.But if you again register using same device then previous one is useless.
So i will suggest better to keep information that device is already registered or not.If registered then no need to re-register again.As you will get notification on the base of your old R_ID. simply send this R_ID to your server for notification purpose and save flag in your data base to keep track of registeration
Your only suppose to request a registration ID when you don't have one and send it to your server(the one that will send C2DM messages). It's strange that you keep getting a new id, in my own testing every time I register and unregister this same app on the same device I get the same id. I haven't done a lot of testing yet, but I would assume the id would change for every device and app combo.
I am saving the registration_id with the device_id in the DB. That way I have only one entry for each device. Also the reg id is saved on the device as a shared preference so that I don't have to request a new one each time...
But I would like to know to if there is some way to find out if the reg id the user has on it's device is expired or not.
Any ideas? Is there an intent for it?
If you register with the cloud, the returned registration ID will be valid until you receive the intent with the "unregistered" string extra (which will only occur if your application consciously unregisters with the service) OR Google sends another Registration intent with a new registration ID, in which case the system you have setup should work fine.
Your server will know if the registration ID has expired if it receives a 200 response with data "Error=NotRegistered". In this case, you would remove that entry from your DB.
http://code.google.com/android/c2dm/#server
The registration ID for the device should be requested from the C2DM server only the first time the app is run after installation and then saved in the database( preferably along with the device ID so that it is unique).
The registration ID is valid as long as C2DM does not send the device a new registration ID, or the user uninstalls the app. I'm assuming you know how to handle the latter case.
Coming back to the first case. It is possible that during the lifetime of the application in the device the C2DM server may send the device a new registration ID[It will do so on its own and you do not have to request again for it]. Thus you should have a listener for the same, and you can update the registration ID of that device in your database.
Hope it helps.