Upstream version 10.38.222.0
[platform/framework/web/crosswalk.git] / src / third_party / cacheinvalidation / src / java / com / google / ipc / invalidation / ticl / android / AndroidInvalidationService.java
1 /*
2  * Copyright 2011 Google Inc.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 package com.google.ipc.invalidation.ticl.android;
18
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;
34
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;
40
41 import java.util.Map;
42 import java.util.concurrent.atomic.AtomicInteger;
43 import java.util.concurrent.atomic.AtomicReference;
44
45
46 /**
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}).
54  *
55  */
56 public class AndroidInvalidationService extends AbstractInvalidationService {
57
58   /**
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.
62    */
63   public static class ReceiverService extends MultiplexingGcmListener.AbstractListener {
64
65     /**
66      * Receiver for broadcasts by the multiplexed GCM service. It forwards them to
67      * AndroidMessageReceiverService.
68      */
69     public static class Receiver extends MultiplexingGcmListener.AbstractListener.Receiver {
70       /* This class is public so that it can be instantiated by the Android runtime. */
71       @Override
72       protected Class<?> getServiceClass() {
73         return ReceiverService.class;
74       }
75     }
76
77     public ReceiverService() {
78       super("MsgRcvrSvc");
79     }
80
81     @Override
82     public void onRegistered(String registrationId) {
83       logger.info("GCM Registration received: %s", registrationId);
84
85       // Upon receiving a new updated GCM ID, notify the invalidation service
86       Intent serviceIntent =
87           AndroidInvalidationService.createRegistrationIntent(this, registrationId);
88       startService(serviceIntent);
89     }
90
91     @Override
92     public void onUnregistered(String registrationId) {
93       logger.info("GCM unregistered");
94     }
95
96     @Override
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);
104         return;
105       }
106       String encodedData = intent.getStringExtra(AndroidC2DMConstants.CONTENT_PARAM);
107       String echoToken = intent.getStringExtra(AndroidC2DMConstants.ECHO_PARAM);
108       if (encodedData != null) {
109         try {
110           byte [] rawData = Base64.decode(encodedData, Base64.URL_SAFE);
111           serviceIntent = AndroidInvalidationService.createDataIntent(this, clientKey, echoToken,
112               rawData);
113         } catch (IllegalArgumentException exception) {
114           logger.severe("Unable to decode intent data", exception);
115           return;
116         }
117       } else {
118         logger.severe("Received mailbox intent: %s", intent);
119         return;
120       }
121       startService(serviceIntent);
122     }
123
124     @Override
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.
128     }
129   }
130
131   /** The last created instance, for testing. */
132   
133   static AtomicReference<AndroidInvalidationService> lastInstanceForTest =
134       new AtomicReference<AndroidInvalidationService>();
135
136   /** For tests only, the number of C2DM errors received. */
137   static final AtomicInteger numGcmErrorsForTest = new AtomicInteger(0);
138
139   /** For tests only, the number of C2DM registration messages received. */
140   static final AtomicInteger numGcmRegistrationForTest = new AtomicInteger(0);
141
142   /** For tests only, the number of C2DM messages received. */
143   static final AtomicInteger numGcmMessagesForTest = new AtomicInteger(0);
144
145   /** For tests only, the number of onCreate calls made. */
146   static final AtomicInteger numCreateForTest = new AtomicInteger(0);
147
148   /** The client manager tracking in-memory client instances */
149   
150   protected static AndroidClientManager clientManager;
151
152   private static final Logger logger = AndroidLogger.forTag("InvService");
153
154   /** The HTTP URL of the channel service. */
155   private static String channelUrl = AndroidHttpConstants.CHANNEL_URL;
156
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.
161
162   /**
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.
166    */
167   static final String REGISTRATION_ACTION = "register";
168
169   /**
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).
173    */
174   static final String REGISTER_ID = "id";
175
176   /**
177    * This intent is sent when a GCM message targeting the service is received.
178    */
179   static final String MESSAGE_ACTION = "message";
180
181   /**
182    * The name of the String extra that contains the client key for the GCM message.
183    */
184   static final String MESSAGE_CLIENT_KEY = "clientKey";
185
186   /**
187    * The name of the byte array extra that contains the encoded event for the GCM message.
188    */
189   static final String MESSAGE_DATA = "data";
190
191   /** The name of the string extra that contains the echo token in the GCM message. */
192   static final String MESSAGE_ECHO = "echo-token";
193
194   /**
195    * This intent is sent when GCM registration has failed irrevocably.
196    */
197   static final String ERROR_ACTION = "error";
198
199   /**
200    * The name of the String extra that contains the error message describing the registration
201    * failure.
202    */
203   static final String ERROR_MESSAGE = "message";
204
205   /** Returns the client manager for this service */
206   static AndroidClientManager getClientManager() {
207     return clientManager;
208   }
209
210   /**
211    * Creates a new registration intent that notifies the service of a registration ID change
212    */
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);
218     }
219     return intent;
220   }
221
222   /**
223    * Creates a new message intent to contains event data to deliver directly to a client.
224    */
225   static Intent createDataIntent(Context context, String clientKey, String token,
226       byte [] data) {
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);
231     if (token != null) {
232       intent.putExtra(MESSAGE_ECHO, token);
233     }
234     return intent;
235   }
236
237   /**
238    * Creates a new message intent that references event data to retrieve from a mailbox.
239    */
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);
244     if (token != null) {
245       intent.putExtra(MESSAGE_ECHO, token);
246     }
247     return intent;
248   }
249
250   /**
251    * Creates a new error intent that notifies the service of a registration failure.
252    */
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);
257     return intent;
258   }
259
260   /**
261    * Overrides the channel URL set in package metadata to enable dynamic port assignment and
262    * configuration during testing.
263    */
264   
265   static void setChannelUrlForTest(String url) {
266     channelUrl = url;
267   }
268
269   /**
270    * Resets the state of the service to destroy any existing clients
271    */
272   
273   static void reset() {
274     if (clientManager != null) {
275       clientManager.releaseAll();
276     }
277   }
278
279   /** The function for computing persistence state digests when rewriting them. */
280   private final DigestFunction digestFn = new ObjectIdDigestUtils.Sha1DigestFunction();
281
282   public AndroidInvalidationService() {
283     lastInstanceForTest.set(this);
284   }
285
286   @Override
287   public void onCreate() {
288     synchronized (lock) {
289       super.onCreate();
290
291       // Create the client manager
292       if (clientManager == null) {
293         clientManager = new AndroidClientManager(this);
294       }
295       numCreateForTest.incrementAndGet();
296     }
297   }
298
299   @Override
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())) {
310         handleError(intent);
311       }
312       final int retval = super.onStartCommand(intent, flags, startId);
313
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());
320       }
321       return retval;
322     }
323   }
324
325   @Override
326   public void onDestroy() {
327     synchronized (lock) {
328       reset();
329       super.onDestroy();
330     }
331   }
332
333   @Override
334   public IBinder onBind(Intent intent) {
335     return super.onBind(intent);
336   }
337
338   @Override
339   public boolean onUnbind(Intent intent) {
340     synchronized (lock) {
341       logger.fine("onUnbind");
342       super.onUnbind(intent);
343
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");
347       }
348       stopServiceIfNoClientsRemain("onUnbind");
349
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.
353       return true;
354     }
355   }
356
357   // The following protected methods are called holding "lock" by AbstractInvalidationService.
358
359   @Override
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);
368   }
369
370   @Override
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());
377     }
378   }
379
380   @Override
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)) {
385       client.start();
386     }
387   }
388
389   @Override
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)) {
394       client.stop();
395     }
396   }
397
398   @Override
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);
405     }
406   }
407
408   @Override
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);
415     }
416   }
417
418   @Override
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);
425     }
426   }
427
428   @Override
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)) {
433       client.destroy();
434     }
435   }
436
437   /**
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}.   *
441    */
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);
447       return false;
448     } else {
449       response.setStatus(Status.SUCCESS);
450       return true;
451     }
452   }
453
454   /** Returns the base URL used to send messages to the outbound network channel */
455   String getChannelUrl() {
456     synchronized (lock) {
457       return channelUrl;
458     }
459   }
460
461   private void handleMessage(Intent intent) {
462     numGcmMessagesForTest.incrementAndGet();
463     String clientKey = intent.getStringExtra(MESSAGE_CLIENT_KEY);
464     AndroidClientProxy proxy = clientManager.get(clientKey);
465
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);
471       return;
472     }
473
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);
478
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);
483     } else {
484       logger.severe("Got mailbox intent: %s", intent);
485     }
486   }
487
488   /**
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.
495    */
496   private void handleGcmMessageForUnstartedClient(AndroidClientProxy proxy) {
497     if (proxy == null) {
498       // Unknown client; nothing to do.
499       return;
500     }
501
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
508     // anything else.
509     final String clientKey = proxy.getClientKey();
510     logger.info("Received message for unloaded client; rewriting state file: %s", clientKey);
511
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.
518       return;
519     }
520
521     // Rewrite the last message sent time.
522     PersistentTiclState newState = PersistentTiclState.newBuilder(clientState)
523         .setLastMessageSendTimeMs(0).build();
524
525     // Serialize the new state.
526     byte[] newClientState = PersistenceUtils.serializeState(newState, digestFn);
527
528     // Write it out.
529     storageForClient.getPropertiesUnsafe().put(InvalidationClientCore.CLIENT_TOKEN_KEY,
530         newClientState);
531     storageForClient.storeUnsafe();
532   }
533
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();
539   }
540
541   private void handleError(Intent intent) {
542     logger.severe("Unable to perform GCM registration: %s", intent.getStringExtra(ERROR_MESSAGE));
543     numGcmErrorsForTest.incrementAndGet();
544   }
545
546   /**
547    * Stops the service if there are no clients in the client manager.
548    * @param debugInfo short string describing why the check was made
549    */
550   private void stopServiceIfNoClientsRemain(String debugInfo) {
551     if ((clientManager == null) || clientManager.areAllClientsStopped()) {
552       logger.info("Stopping AndroidInvalidationService since no clients remain: %s", debugInfo);
553       stopSelf();
554     } else {
555       logger.fine("Not stopping service since %s clients remain (%s)",
556           clientManager.getClientCount(), debugInfo);
557     }
558   }
559
560   /**
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.
563    * <p>
564    * REQUIRES: {@code storageForClient}.load() has been called successfully.
565    */
566   
567   
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());
577         return null;
578       }
579
580       // Deserialize it.
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);
585         return null;
586       }
587       return clientState;
588     }
589   }
590
591   /**
592    * Returns whether the client with {@code clientKey} is loaded in the client manager.
593    */
594   public static boolean isLoadedForTest(String clientKey) {
595     return (getClientManager() != null) && getClientManager().isLoadedForTest(clientKey);
596   }
597 }