Upstream version 10.38.222.0
[platform/framework/web/crosswalk.git] / src / third_party / cacheinvalidation / src / java / com / google / ipc / invalidation / ticl / android / AndroidChannel.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.android.gcm.GCMRegistrar;
20 import com.google.common.base.Preconditions;
21 import com.google.ipc.invalidation.common.CommonProtos2;
22 import com.google.ipc.invalidation.external.client.SystemResources;
23 import com.google.ipc.invalidation.external.client.SystemResources.Logger;
24 import com.google.ipc.invalidation.external.client.SystemResources.NetworkChannel;
25 import com.google.ipc.invalidation.external.client.android.service.AndroidLogger;
26 import com.google.ipc.invalidation.ticl.TestableNetworkChannel;
27 import com.google.ipc.invalidation.util.ExponentialBackoffDelayGenerator;
28 import com.google.protobuf.InvalidProtocolBufferException;
29 import com.google.protos.ipc.invalidation.AndroidChannel.AddressedAndroidMessage;
30 import com.google.protos.ipc.invalidation.AndroidChannel.MajorVersion;
31 import com.google.protos.ipc.invalidation.ChannelCommon.NetworkEndpointId;
32 import com.google.protos.ipc.invalidation.ClientProtocol.Version;
33
34 import android.accounts.AccountManager;
35 import android.accounts.AccountManagerCallback;
36 import android.accounts.AccountManagerFuture;
37 import android.accounts.AuthenticatorException;
38 import android.accounts.OperationCanceledException;
39 import android.content.Context;
40 import android.net.http.AndroidHttpClient;
41 import android.os.Build;
42 import android.os.Bundle;
43 import android.util.Base64;
44
45 import org.apache.http.client.HttpClient;
46
47 import java.io.IOException;
48 import java.util.ArrayList;
49 import java.util.List;
50 import java.util.Random;
51 import java.util.concurrent.ExecutorService;
52 import java.util.concurrent.Executors;
53 import java.util.concurrent.atomic.AtomicInteger;
54
55
56 /**
57  * Provides a bidirectional channel for Android devices using GCM (data center to device) and the
58  * Android HTTP frontend (device to data center). The android channel computes a network endpoint id
59  * based upon the GCM registration ID for the containing application ID and the client key of the
60  * client using the channel. If an attempt is made to send messages on the channel before a GCM
61  * registration ID has been assigned, it will temporarily buffer the outbound messages and send them
62  * when the registration ID is eventually assigned.
63  *
64  */
65 class AndroidChannel extends AndroidChannelBase implements TestableNetworkChannel {
66
67   private static final Logger logger = AndroidLogger.forTag("InvChannel");
68
69   /**
70    * The maximum number of outbound messages that will be buffered while waiting for async delivery
71    * of the GCM registration ID and authentication token.   The general flow for a new client is
72    * that an 'initialize' message is sent (and resent on a timed interval) until a session token is
73    * sent back and this just prevents accumulation a large number of initialize messages (and
74    * consuming memory) in a long delay or failure scenario.
75    */
76   private static final int MAX_BUFFERED_MESSAGES = 10;
77
78   /** The channel version expected by this channel implementation */
79   
80   static final Version CHANNEL_VERSION =
81       CommonProtos2.newVersion(MajorVersion.INITIAL.getNumber(), 0);
82
83   /** How to long to wait initially before retrying a failed auth token request. */
84   private static final int INITIAL_AUTH_TOKEN_RETRY_DELAY_MS = 1 * 1000;  // 1 second
85
86   /** Largest exponential backoff factor to use for auth token retries. */
87   private static final int MAX_AUTH_TOKEN_RETRY_FACTOR = 60 * 60 * 12; // 12 hours
88
89   /** Number of C2DM messages for unknown clients. */
90   
91   static final AtomicInteger numGcmInvalidClients = new AtomicInteger();
92
93   /** Invalidation client proxy using the channel. */
94   private final AndroidClientProxy proxy;
95
96   /** Android context used to retrieve registration IDs. */
97   private final Context context;
98
99   /** System resources for this channel */
100   private SystemResources resources;
101
102   /**
103    * When set, this registration ID is used rather than checking
104    * {@link GCMRegistrar#getRegistrationId}. It should not be read directly: call
105    * {@link #getRegistrationId} instead.
106    */
107   private String registrationIdForTest;
108
109   /** The authentication token that can be used in channel requests to the server */
110   private String authToken;
111
112   /** Listener for network events. */
113   private NetworkChannel.NetworkListener listener;
114
115   // TODO: Add code to track time of last network activity (in either direction)
116   // so inactive clients can be detected and periodically flushed from memory.
117
118   /**
119    * List that holds outbound messages while waiting for a registration ID.   Allocated on
120    * demand since it is only needed when there is no registration id.
121    */
122   private List<byte[]> pendingMessages = null;
123
124   /**
125    * Testing only flag that disables interactions with the AcccountManager for mock tests.
126    */
127    static boolean disableAccountManager = false;
128
129   /**
130    * Returns the default HTTP client to use for requests from the channel based upon its execution
131    * context.  The format of the User-Agent string is "<application-pkg>(<android-release>)".
132    */
133   static AndroidHttpClient getDefaultHttpClient(Context context) {
134     return AndroidHttpClient.newInstance(
135        context.getApplicationInfo().className + "(" + Build.VERSION.RELEASE + ")");
136   }
137
138   /** Executor used for HTTP calls to send messages to . */
139   
140   final ExecutorService scheduler = Executors.newSingleThreadExecutor();
141
142   /**
143    * Creates a new AndroidChannel.
144    *
145    * @param proxy the client proxy associated with the channel
146    * @param httpClient the HTTP client to use to communicate with the Android invalidation frontend
147    * @param context Android context
148    */
149   AndroidChannel(AndroidClientProxy proxy, HttpClient httpClient, Context context) {
150     super(httpClient, proxy.getAuthType(), proxy.getService().getChannelUrl());
151     this.proxy = Preconditions.checkNotNull(proxy);
152     this.context = Preconditions.checkNotNull(context);
153   }
154
155   /**
156    * Returns the GCM registration ID associated with the channel. Checks the {@link GCMRegistrar}
157    * unless {@link #setRegistrationIdForTest} has been called.
158    */
159   String getRegistrationId() {
160     String registrationId = (registrationIdForTest != null) ? registrationIdForTest :
161         GCMRegistrar.getRegistrationId(context);
162
163     // Callers check for null registration ID rather than "null or empty", so replace empty strings
164     // with null here.
165     if ("".equals(registrationId)) {
166       registrationId = null;
167     }
168     return registrationId;
169   }
170
171   /** Returns the client proxy that is using the channel */
172    AndroidClientProxy getClientProxy() {
173     return proxy;
174   }
175
176   /**
177    * Retrieves the list of pending messages in the channel (or {@code null} if there are none).
178    */
179    List<byte[]> getPendingMessages() {
180     return pendingMessages;
181   }
182
183   @Override
184   
185   protected String getAuthToken() {
186     return authToken;
187   }
188
189   /** A completion callback for an asynchronous operation. */
190   interface CompletionCallback {
191     void success();
192     void failure();
193   }
194
195   /** An asynchronous runnable that calls a completion callback. */
196   interface AsyncRunnable {
197     void run(CompletionCallback callback);
198   }
199
200   /**
201    * A utility function to run an async runnable with exponential backoff after failures.
202    * @param runnable the asynchronous runnable.
203    * @param scheduler used to schedule retries.
204    * @param backOffGenerator a backoff generator that returns how to long to wait between retries.
205    *     The client must pass a new instance or reset the backoff generator before calling this
206    *     method.
207    */
208   
209   static void retryUntilSuccessWithBackoff(final SystemResources.Scheduler scheduler,
210       final ExponentialBackoffDelayGenerator backOffGenerator, final AsyncRunnable runnable) {
211     logger.fine("Running %s", runnable);
212     runnable.run(new CompletionCallback() {
213         @Override
214         public void success() {
215           logger.fine("%s succeeded", runnable);
216         }
217
218         @Override
219         public void failure() {
220           int nextDelay = backOffGenerator.getNextDelay();
221           logger.fine("%s failed, retrying after %s ms", nextDelay);
222           scheduler.schedule(nextDelay, new Runnable() {
223             @Override
224             public void run() {
225               retryUntilSuccessWithBackoff(scheduler, backOffGenerator, runnable);
226             }
227           });
228         }
229     });
230   }
231
232   /**
233    * Initiates acquisition of an authentication token that can be used with channel HTTP requests.
234    * Android token acquisition is asynchronous since it may require HTTP interactions with the
235    * ClientLogin servers to obtain the token.
236    */
237   @SuppressWarnings("deprecation")
238   
239   synchronized void requestAuthToken(final CompletionCallback callback) {
240     // If there is currently no token and no pending request, initiate one.
241     if (disableAccountManager) {
242       logger.fine("Not requesting auth token since account manager disabled");
243       return;
244     }
245     if (authToken == null) {
246       // Ask the AccountManager for the token, with a pending future to store it on the channel
247       // once available.
248       final AndroidChannel theChannel = this;
249       AccountManager accountManager = AccountManager.get(proxy.getService());
250       accountManager.getAuthToken(proxy.getAccount(), proxy.getAuthType(), true,
251           new AccountManagerCallback<Bundle>() {
252             @Override
253             public void run(AccountManagerFuture<Bundle> future) {
254               try {
255                 Bundle result = future.getResult();
256                 if (result.containsKey(AccountManager.KEY_INTENT)) {
257                   // TODO: Handle case where there are no authentication
258                   // credentials associated with the client account
259                   logger.severe("Token acquisition requires user login");
260                   callback.success(); // No further retries.
261                 }
262                 setAuthToken(result.getString(AccountManager.KEY_AUTHTOKEN));
263               } catch (OperationCanceledException exception) {
264                 logger.warning("Auth cancelled", exception);
265                 // TODO: Send error to client
266               } catch (AuthenticatorException exception) {
267                 logger.warning("Auth error acquiring token", exception);
268                 callback.failure();
269               } catch (IOException exception) {
270                 logger.warning("IO Exception acquiring token", exception);
271                 callback.failure();
272               }
273             }
274       }, null);
275     } else {
276       logger.fine("Auth token request already pending");
277       callback.success();
278     }
279   }
280
281   /*
282    * Updates the registration ID for this channel, flushing any pending outbound messages that
283    * were waiting for an id.
284    */
285   synchronized void setRegistrationIdForTest(String updatedRegistrationId) {
286     // Synchronized to avoid concurrent access to pendingMessages
287     if (registrationIdForTest != updatedRegistrationId) {
288       logger.fine("Setting registration ID for test for client key %s", proxy.getClientKey());
289       registrationIdForTest = updatedRegistrationId;
290       informRegistrationIdChanged();
291     }
292   }
293
294   /**
295    * Call to inform the Android channel that the registration ID has changed. May kick loose some
296    * pending outbound messages.
297    */
298   synchronized void informRegistrationIdChanged() {
299     checkReady();
300   }
301
302   /**
303    * Sets the authentication token to use for HTTP requests to the invalidation frontend and
304    * flushes any pending messages (if appropriate).
305    *
306    * @param authToken the authentication token
307    */
308   synchronized void setAuthToken(String authToken) {
309     logger.fine("Auth token received fo %s", proxy.getClientKey());
310     this.authToken = authToken;
311     checkReady();
312   }
313
314   @Override
315   public void setListener(NetworkChannel.NetworkListener listener) {
316     this.listener = Preconditions.checkNotNull(listener);
317   }
318
319   @Override
320   public synchronized void sendMessage(final byte[] outgoingMessage) {
321     // synchronized to avoid concurrent access to pendingMessages
322
323     // If there is no registration id, we cannot compute a network endpoint id. If there is no
324     // auth token, then we cannot authenticate the send request.  Defer sending messages until both
325     // are received.
326     String registrationId = getRegistrationId();
327     if ((registrationId == null) || (authToken == null)) {
328       if (pendingMessages == null) {
329         pendingMessages = new ArrayList<byte[]>();
330       }
331       logger.fine("Buffering outbound message: hasRegId: %s, hasAuthToken: %s",
332           registrationId != null, authToken != null);
333       if (pendingMessages.size() < MAX_BUFFERED_MESSAGES) {
334         pendingMessages.add(outgoingMessage);
335       } else {
336         logger.warning("Exceeded maximum number of buffered messages, dropping outbound message");
337       }
338       return;
339     }
340
341     // Do the actual HTTP I/O on a separate thread, since we may be called on the main
342     // thread for the application.
343     scheduler.execute(new Runnable() {
344       @Override
345       public void run() {
346         if (resources.isStarted()) {
347           deliverOutboundMessage(outgoingMessage);
348         } else {
349           logger.warning("Dropping outbound messages because resources are stopped");
350         }
351       }
352     });
353   }
354
355   /**
356    * Called when either the registration or authentication token has been received to check to
357    * see if channel is ready for network activity.  If so, the status receiver is notified and
358    * any pending messages are flushed.
359    */
360   private synchronized void checkReady() {
361     String registrationId = getRegistrationId();
362     if ((registrationId != null) && (authToken != null)) {
363
364       logger.fine("Enabling network endpoint: %s", getWebEncodedEndpointId());
365
366       // Notify the network listener that we are now network enabled
367       if (listener != null) {
368         listener.onOnlineStatusChange(true);
369       }
370
371       // Flush any pending messages
372       if (pendingMessages != null) {
373         for (byte [] message : pendingMessages) {
374           sendMessage(message);
375         }
376         pendingMessages = null;
377       }
378     }
379   }
380
381   void receiveMessage(byte[] inboundMessage) {
382     try {
383       AddressedAndroidMessage addrMessage = AddressedAndroidMessage.parseFrom(inboundMessage);
384       tryDeliverMessage(addrMessage);
385     } catch (InvalidProtocolBufferException exception) {
386       logger.severe("Failed decoding AddressedAndroidMessage as C2DM payload", exception);
387     }
388   }
389
390   /**
391    * Delivers the payload of {@code addrMessage} to the {@code callbackReceiver} if the client key
392    * of the addressed message matches that of the {@link #proxy}.
393    */
394   @Override
395   protected void tryDeliverMessage(AddressedAndroidMessage addrMessage) {
396     String clientKey = proxy.getClientKey();
397     if (addrMessage.getClientKey().equals(clientKey)) {
398       logger.fine("Deliver to %s message %s", clientKey, addrMessage);
399       listener.onMessageReceived(addrMessage.getMessage().toByteArray());
400     } else {
401       logger.severe("Not delivering message due to key mismatch: %s vs %s",
402           addrMessage.getClientKey(), clientKey);
403       numGcmInvalidClients.incrementAndGet();
404     }
405   }
406
407   /** Returns the web encoded version of the channel network endpoint ID for HTTP requests. */
408   @Override
409   protected String getWebEncodedEndpointId() {
410     NetworkEndpointId networkEndpointId = getNetworkId();
411     return Base64.encodeToString(networkEndpointId.toByteArray(),
412         Base64.URL_SAFE | Base64.NO_WRAP  | Base64.NO_PADDING);
413   }
414
415   @Override
416   public void setSystemResources(SystemResources resources) {
417     this.resources = resources;
418
419     // Prefetch the auth sub token.  Since this might require an HTTP round trip, we do this
420     // as soon as the resources are available.
421     // TODO: Find a better place to fetch the auth token; this method
422     // doesn't sound like one that should be doing work.
423     retryUntilSuccessWithBackoff(resources.getInternalScheduler(),
424         new ExponentialBackoffDelayGenerator(
425             new Random(), INITIAL_AUTH_TOKEN_RETRY_DELAY_MS, MAX_AUTH_TOKEN_RETRY_FACTOR),
426         new AsyncRunnable() {
427           @Override
428           public void run(CompletionCallback callback) {
429             requestAuthToken(callback);
430           }
431         });
432   }
433
434   @Override
435   public NetworkEndpointId getNetworkIdForTest() {
436     return getNetworkId();
437   }
438
439   @Override
440   protected Logger getLogger() {
441     return resources.getLogger();
442   }
443
444   private NetworkEndpointId getNetworkId() {
445     String registrationId = getRegistrationId();
446     return CommonProtos2.newAndroidEndpointId(registrationId, proxy.getClientKey(),
447         proxy.getService().getPackageName(), CHANNEL_VERSION);
448   }
449
450   ExecutorService getExecutorServiceForTest() {
451     return scheduler;
452   }
453
454   @Override
455   void setHttpClientForTest(HttpClient client) {
456     if (this.httpClient instanceof AndroidHttpClient) {
457       // Release the previous client if any.
458       ((AndroidHttpClient) this.httpClient).close();
459     }
460     super.setHttpClientForTest(client);
461   }
462 }