2 * Copyright 2011 Google Inc.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.google.ipc.invalidation.ticl.android;
19 import com.google.ipc.invalidation.common.DigestFunction;
20 import com.google.ipc.invalidation.common.ObjectIdDigestUtils;
21 import com.google.ipc.invalidation.external.client.SystemResources.Logger;
22 import com.google.ipc.invalidation.external.client.android.AndroidInvalidationClient;
23 import com.google.ipc.invalidation.external.client.android.service.AndroidLogger;
24 import com.google.ipc.invalidation.external.client.android.service.Request;
25 import com.google.ipc.invalidation.external.client.android.service.Response;
26 import com.google.ipc.invalidation.external.client.android.service.Response.Status;
27 import com.google.ipc.invalidation.external.client.contrib.MultiplexingGcmListener;
28 import com.google.ipc.invalidation.external.client.types.AckHandle;
29 import com.google.ipc.invalidation.external.client.types.ObjectId;
30 import com.google.ipc.invalidation.ticl.InvalidationClientCore;
31 import com.google.ipc.invalidation.ticl.PersistenceUtils;
32 import com.google.ipc.invalidation.util.TypedUtil;
33 import com.google.protos.ipc.invalidation.Client.PersistentTiclState;
35 import android.accounts.Account;
36 import android.content.Context;
37 import android.content.Intent;
38 import android.os.IBinder;
39 import android.util.Base64;
42 import java.util.concurrent.atomic.AtomicInteger;
43 import java.util.concurrent.atomic.AtomicReference;
47 * The AndroidInvalidationService class provides an Android service implementation that bridges
48 * between the {@code InvalidationService} interface and invalidation client service instances
49 * executing within the scope of that service. The invalidation service will have an associated
50 * {@link AndroidClientManager} that is managing the set of active (in memory) clients associated
51 * with the service. It processes requests from invalidation applications (as invocations on
52 * the {@code InvalidationService} bound service interface along with GCM registration and
53 * activity (from {@link ReceiverService}).
56 public class AndroidInvalidationService extends AbstractInvalidationService {
59 * Service that handles system GCM messages (with support from the base class). It receives
60 * intents for GCM registration, errors and message delivery. It does some basic processing and
61 * then forwards the messages to the {@link AndroidInvalidationService} for handling.
63 public static class ReceiverService extends MultiplexingGcmListener.AbstractListener {
66 * Receiver for broadcasts by the multiplexed GCM service. It forwards them to
67 * AndroidMessageReceiverService.
69 public static class Receiver extends MultiplexingGcmListener.AbstractListener.Receiver {
70 /* This class is public so that it can be instantiated by the Android runtime. */
72 protected Class<?> getServiceClass() {
73 return ReceiverService.class;
77 public ReceiverService() {
82 public void onRegistered(String registrationId) {
83 logger.info("GCM Registration received: %s", registrationId);
85 // Upon receiving a new updated GCM ID, notify the invalidation service
86 Intent serviceIntent =
87 AndroidInvalidationService.createRegistrationIntent(this, registrationId);
88 startService(serviceIntent);
92 public void onUnregistered(String registrationId) {
93 logger.info("GCM unregistered");
97 protected void onMessage(Intent intent) {
98 // Extract expected fields and do basic syntactic checks (but no value checking)
99 // and forward the result on to the AndroidInvalidationService for processing.
100 Intent serviceIntent;
101 String clientKey = intent.getStringExtra(AndroidC2DMConstants.CLIENT_KEY_PARAM);
102 if (clientKey == null) {
103 logger.severe("GCM Intent does not contain client key value: %s", intent);
106 String encodedData = intent.getStringExtra(AndroidC2DMConstants.CONTENT_PARAM);
107 String echoToken = intent.getStringExtra(AndroidC2DMConstants.ECHO_PARAM);
108 if (encodedData != null) {
110 byte [] rawData = Base64.decode(encodedData, Base64.URL_SAFE);
111 serviceIntent = AndroidInvalidationService.createDataIntent(this, clientKey, echoToken,
113 } catch (IllegalArgumentException exception) {
114 logger.severe("Unable to decode intent data", exception);
118 logger.severe("Received mailbox intent: %s", intent);
121 startService(serviceIntent);
125 protected void onDeletedMessages(int total) {
126 // This method must be implemented if we start using non-collapsable messages with GCM. For
127 // now, there is nothing to do.
131 /** The last created instance, for testing. */
133 static AtomicReference<AndroidInvalidationService> lastInstanceForTest =
134 new AtomicReference<AndroidInvalidationService>();
136 /** For tests only, the number of C2DM errors received. */
137 static final AtomicInteger numGcmErrorsForTest = new AtomicInteger(0);
139 /** For tests only, the number of C2DM registration messages received. */
140 static final AtomicInteger numGcmRegistrationForTest = new AtomicInteger(0);
142 /** For tests only, the number of C2DM messages received. */
143 static final AtomicInteger numGcmMessagesForTest = new AtomicInteger(0);
145 /** For tests only, the number of onCreate calls made. */
146 static final AtomicInteger numCreateForTest = new AtomicInteger(0);
148 /** The client manager tracking in-memory client instances */
150 protected static AndroidClientManager clientManager;
152 private static final Logger logger = AndroidLogger.forTag("InvService");
154 /** The HTTP URL of the channel service. */
155 private static String channelUrl = AndroidHttpConstants.CHANNEL_URL;
157 // The AndroidInvalidationService handles a set of internal intents that are used for
158 // communication and coordination between the it and the GCM handling service. These
159 // are documented here with action and extra names documented with package private
160 // visibility since they are not intended for use by external components.
163 * Sent when a new GCM registration activity occurs for the service. This can occur the first
164 * time the service is run or at any subsequent time if the Android C2DM service decides to issue
165 * a new GCM registration ID.
167 static final String REGISTRATION_ACTION = "register";
170 * The name of the String extra that contains the registration ID for a register intent. If this
171 * extra is not present, then it indicates that a C2DM notification regarding unregistration has
172 * been received (not expected during normal operation conditions).
174 static final String REGISTER_ID = "id";
177 * This intent is sent when a GCM message targeting the service is received.
179 static final String MESSAGE_ACTION = "message";
182 * The name of the String extra that contains the client key for the GCM message.
184 static final String MESSAGE_CLIENT_KEY = "clientKey";
187 * The name of the byte array extra that contains the encoded event for the GCM message.
189 static final String MESSAGE_DATA = "data";
191 /** The name of the string extra that contains the echo token in the GCM message. */
192 static final String MESSAGE_ECHO = "echo-token";
195 * This intent is sent when GCM registration has failed irrevocably.
197 static final String ERROR_ACTION = "error";
200 * The name of the String extra that contains the error message describing the registration
203 static final String ERROR_MESSAGE = "message";
205 /** Returns the client manager for this service */
206 static AndroidClientManager getClientManager() {
207 return clientManager;
211 * Creates a new registration intent that notifies the service of a registration ID change
213 static Intent createRegistrationIntent(Context context, String registrationId) {
214 Intent intent = new Intent(REGISTRATION_ACTION);
215 intent.setClass(context, AndroidInvalidationService.class);
216 if (registrationId != null) {
217 intent.putExtra(AndroidInvalidationService.REGISTER_ID, registrationId);
223 * Creates a new message intent to contains event data to deliver directly to a client.
225 static Intent createDataIntent(Context context, String clientKey, String token,
227 Intent intent = new Intent(MESSAGE_ACTION);
228 intent.setClass(context, AndroidInvalidationService.class);
229 intent.putExtra(MESSAGE_CLIENT_KEY, clientKey);
230 intent.putExtra(MESSAGE_DATA, data);
232 intent.putExtra(MESSAGE_ECHO, token);
238 * Creates a new message intent that references event data to retrieve from a mailbox.
240 static Intent createMailboxIntent(Context context, String clientKey, String token) {
241 Intent intent = new Intent(MESSAGE_ACTION);
242 intent.setClass(context, AndroidInvalidationService.class);
243 intent.putExtra(MESSAGE_CLIENT_KEY, clientKey);
245 intent.putExtra(MESSAGE_ECHO, token);
251 * Creates a new error intent that notifies the service of a registration failure.
253 static Intent createErrorIntent(Context context, String errorId) {
254 Intent intent = new Intent(ERROR_ACTION);
255 intent.setClass(context, AndroidInvalidationService.class);
256 intent.putExtra(ERROR_MESSAGE, errorId);
261 * Overrides the channel URL set in package metadata to enable dynamic port assignment and
262 * configuration during testing.
265 static void setChannelUrlForTest(String url) {
270 * Resets the state of the service to destroy any existing clients
273 static void reset() {
274 if (clientManager != null) {
275 clientManager.releaseAll();
279 /** The function for computing persistence state digests when rewriting them. */
280 private final DigestFunction digestFn = new ObjectIdDigestUtils.Sha1DigestFunction();
282 public AndroidInvalidationService() {
283 lastInstanceForTest.set(this);
287 public void onCreate() {
288 synchronized (lock) {
291 // Create the client manager
292 if (clientManager == null) {
293 clientManager = new AndroidClientManager(this);
295 numCreateForTest.incrementAndGet();
300 public int onStartCommand(Intent intent, int flags, int startId) {
301 // Process GCM related messages from the ReceiverService. We do not check isCreated here because
302 // this is part of the stop/start lifecycle, not bind/unbind.
303 synchronized (lock) {
304 logger.fine("Received action = %s", intent.getAction());
305 if (MESSAGE_ACTION.equals(intent.getAction())) {
306 handleMessage(intent);
307 } else if (REGISTRATION_ACTION.equals(intent.getAction())) {
308 handleRegistration(intent);
309 } else if (ERROR_ACTION.equals(intent.getAction())) {
312 final int retval = super.onStartCommand(intent, flags, startId);
314 // Unless we are explicitly being asked to start, stop ourselves. Request.SERVICE_INTENT
315 // is the intent used by InvalidationBinder to bind the service, and
316 // AndroidInvalidationClientImpl uses the intent returned by InvalidationBinder.getIntent
317 // as the argument to its startService call.
318 if (!Request.SERVICE_INTENT.getAction().equals(intent.getAction())) {
319 stopServiceIfNoClientsRemain(intent.getAction());
326 public void onDestroy() {
327 synchronized (lock) {
334 public IBinder onBind(Intent intent) {
335 return super.onBind(intent);
339 public boolean onUnbind(Intent intent) {
340 synchronized (lock) {
341 logger.fine("onUnbind");
342 super.onUnbind(intent);
344 if ((clientManager != null) && (clientManager.getClientCount() > 0)) {
345 // This isn't wrong, per se, but it's potentially unusual.
346 logger.info(" clients still active in onUnbind");
348 stopServiceIfNoClientsRemain("onUnbind");
350 // We don't care about the onRebind event, which is what the documentation says a "true"
351 // return here will get us, but if we return false then we don't get a second onUnbind() event
352 // in a bind/unbind/bind/unbind cycle, which we require.
357 // The following protected methods are called holding "lock" by AbstractInvalidationService.
360 protected void create(Request request, Response.Builder response) {
361 String clientKey = request.getClientKey();
362 int clientType = request.getClientType();
363 Account account = request.getAccount();
364 String authType = request.getAuthType();
365 Intent eventIntent = request.getIntent();
366 clientManager.create(clientKey, clientType, account, authType, eventIntent);
367 response.setStatus(Status.SUCCESS);
371 protected void resume(Request request, Response.Builder response) {
372 String clientKey = request.getClientKey();
373 AndroidClientProxy client = clientManager.get(clientKey);
374 if (setResponseStatus(client, request, response)) {
375 response.setAccount(client.getAccount());
376 response.setAuthType(client.getAuthType());
381 protected void start(Request request, Response.Builder response) {
382 String clientKey = request.getClientKey();
383 AndroidInvalidationClient client = clientManager.get(clientKey);
384 if (setResponseStatus(client, request, response)) {
390 protected void stop(Request request, Response.Builder response) {
391 String clientKey = request.getClientKey();
392 AndroidInvalidationClient client = clientManager.get(clientKey);
393 if (setResponseStatus(client, request, response)) {
399 protected void register(Request request, Response.Builder response) {
400 String clientKey = request.getClientKey();
401 AndroidInvalidationClient client = clientManager.get(clientKey);
402 if (setResponseStatus(client, request, response)) {
403 ObjectId objectId = request.getObjectId();
404 client.register(objectId);
409 protected void unregister(Request request, Response.Builder response) {
410 String clientKey = request.getClientKey();
411 AndroidInvalidationClient client = clientManager.get(clientKey);
412 if (setResponseStatus(client, request, response)) {
413 ObjectId objectId = request.getObjectId();
414 client.unregister(objectId);
419 protected void acknowledge(Request request, Response.Builder response) {
420 String clientKey = request.getClientKey();
421 AckHandle ackHandle = request.getAckHandle();
422 AndroidInvalidationClient client = clientManager.get(clientKey);
423 if (setResponseStatus(client, request, response)) {
424 client.acknowledge(ackHandle);
429 protected void destroy(Request request, Response.Builder response) {
430 String clientKey = request.getClientKey();
431 AndroidInvalidationClient client = clientManager.get(clientKey);
432 if (setResponseStatus(client, request, response)) {
438 * If {@code client} is {@code null}, sets the {@code response} status to an error. Otherwise,
439 * sets the status to {@code success}.
440 * @return whether {@code client} was non-{@code null}. *
442 private boolean setResponseStatus(AndroidInvalidationClient client, Request request,
443 Response.Builder response) {
444 if (client == null) {
445 response.setError("Client does not exist: " + request);
446 response.setStatus(Status.INVALID_CLIENT);
449 response.setStatus(Status.SUCCESS);
454 /** Returns the base URL used to send messages to the outbound network channel */
455 String getChannelUrl() {
456 synchronized (lock) {
461 private void handleMessage(Intent intent) {
462 numGcmMessagesForTest.incrementAndGet();
463 String clientKey = intent.getStringExtra(MESSAGE_CLIENT_KEY);
464 AndroidClientProxy proxy = clientManager.get(clientKey);
466 // Client is unknown or unstarted; we can't deliver the message, but we need to
467 // remember that we dropped it if the client is known.
468 if ((proxy == null) || !proxy.isStarted()) {
469 logger.warning("Dropping GCM message for unknown or unstarted client: %s", clientKey);
470 handleGcmMessageForUnstartedClient(proxy);
474 // We can deliver the message. Pass the new echo token to the channel.
475 String echoToken = intent.getStringExtra(MESSAGE_ECHO);
476 logger.fine("Update %s with new echo token: %s", clientKey, echoToken);
477 proxy.getChannel().updateEchoToken(echoToken);
479 byte [] message = intent.getByteArrayExtra(MESSAGE_DATA);
480 if (message != null) {
481 logger.fine("Deliver to %s message %s", clientKey, message);
482 proxy.getChannel().receiveMessage(message);
484 logger.severe("Got mailbox intent: %s", intent);
489 * Handles receipt of a GCM message for a client that was unknown or not started. If the client
490 * was unknown, drops the message. If the client was not started, rewrites the client's
491 * persistent state to have a last-message-sent-time of 0, ensuring that the client will
492 * send a heartbeat to the server when restarted. Since we drop the received GCM message,
493 * the client will be disconnected by the invalidation pusher; this heartbeat ensures a
494 * timely reconnection.
496 private void handleGcmMessageForUnstartedClient(AndroidClientProxy proxy) {
498 // Unknown client; nothing to do.
502 // Client is not started. Open its storage. We are going to use unsafe calls here that
503 // bypass the normal storage API. This is safe in this context because we hold a lock
504 // that prevents anyone else from starting this client or accessing its storage. We
505 // really should not be holding a lock across I/O, but at least this is only local
506 // file I/O, and we're only writing a few bytes. Additionally, since we currently only
507 // have one Ticl, we should only ever enter this function if we're not being used for
509 final String clientKey = proxy.getClientKey();
510 logger.info("Received message for unloaded client; rewriting state file: %s", clientKey);
512 // This storage must have been loaded, because we got this proxy from the client manager,
513 // which always ensures that its entries have that property.
514 AndroidStorage storageForClient = proxy.getStorage();
515 PersistentTiclState clientState = decodeTiclState(clientKey, storageForClient);
516 if (clientState == null) {
517 // Logging done in decodeTiclState.
521 // Rewrite the last message sent time.
522 PersistentTiclState newState = PersistentTiclState.newBuilder(clientState)
523 .setLastMessageSendTimeMs(0).build();
525 // Serialize the new state.
526 byte[] newClientState = PersistenceUtils.serializeState(newState, digestFn);
529 storageForClient.getPropertiesUnsafe().put(InvalidationClientCore.CLIENT_TOKEN_KEY,
531 storageForClient.storeUnsafe();
534 private void handleRegistration(Intent intent) {
535 // Notify the client manager of the updated registration ID
536 String id = intent.getStringExtra(REGISTER_ID);
537 clientManager.informRegistrationIdChanged();
538 numGcmRegistrationForTest.incrementAndGet();
541 private void handleError(Intent intent) {
542 logger.severe("Unable to perform GCM registration: %s", intent.getStringExtra(ERROR_MESSAGE));
543 numGcmErrorsForTest.incrementAndGet();
547 * Stops the service if there are no clients in the client manager.
548 * @param debugInfo short string describing why the check was made
550 private void stopServiceIfNoClientsRemain(String debugInfo) {
551 if ((clientManager == null) || clientManager.areAllClientsStopped()) {
552 logger.info("Stopping AndroidInvalidationService since no clients remain: %s", debugInfo);
555 logger.fine("Not stopping service since %s clients remain (%s)",
556 clientManager.getClientCount(), debugInfo);
561 * Returns the persisted state for the client with key {@code clientKey} in
562 * {@code storageForClient}, or {@code null} if no valid state could be found.
564 * REQUIRES: {@code storageForClient}.load() has been called successfully.
568 PersistentTiclState decodeTiclState(final String clientKey, AndroidStorage storageForClient) {
569 synchronized (lock) {
570 // Retrieve the serialized state.
571 final Map<String, byte[]> properties = storageForClient.getPropertiesUnsafe();
572 byte[] clientStateBytes = TypedUtil.mapGet(properties,
573 InvalidationClientCore.CLIENT_TOKEN_KEY);
574 if (clientStateBytes == null) {
575 logger.warning("No client state found in storage for %s: %s", clientKey,
576 properties.keySet());
581 PersistentTiclState clientState =
582 PersistenceUtils.deserializeState(logger, clientStateBytes, digestFn);
583 if (clientState == null) {
584 logger.warning("Invalid client state found in storage for %s", clientKey);
592 * Returns whether the client with {@code clientKey} is loaded in the client manager.
594 public static boolean isLoadedForTest(String clientKey) {
595 return (getClientManager() != null) && getClientManager().isLoadedForTest(clientKey);