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.android2;
19 import com.google.ipc.invalidation.common.DigestFunction;
20 import com.google.ipc.invalidation.common.ObjectIdDigestUtils;
21 import com.google.ipc.invalidation.external.client.types.AckHandle;
22 import com.google.ipc.invalidation.external.client.types.Callback;
23 import com.google.ipc.invalidation.external.client.types.ErrorInfo;
24 import com.google.ipc.invalidation.external.client.types.ObjectId;
25 import com.google.ipc.invalidation.external.client.types.SimplePair;
26 import com.google.ipc.invalidation.external.client.types.Status;
27 import com.google.ipc.invalidation.ticl.InvalidationClientCore;
28 import com.google.ipc.invalidation.ticl.PersistenceUtils;
29 import com.google.ipc.invalidation.ticl.ProtoWrapperConverter;
30 import com.google.ipc.invalidation.ticl.android2.AndroidInvalidationClientImpl.IntentForwardingListener;
31 import com.google.ipc.invalidation.ticl.android2.ResourcesFactory.AndroidResources;
32 import com.google.ipc.invalidation.ticl.proto.AndroidService.AndroidSchedulerEvent;
33 import com.google.ipc.invalidation.ticl.proto.AndroidService.ClientDowncall;
34 import com.google.ipc.invalidation.ticl.proto.AndroidService.ClientDowncall.RegistrationDowncall;
35 import com.google.ipc.invalidation.ticl.proto.AndroidService.InternalDowncall;
36 import com.google.ipc.invalidation.ticl.proto.AndroidService.InternalDowncall.CreateClient;
37 import com.google.ipc.invalidation.ticl.proto.Client.PersistentTiclState;
38 import com.google.ipc.invalidation.ticl.proto.ClientProtocol.ServerToClientMessage;
39 import com.google.ipc.invalidation.util.Bytes;
40 import com.google.ipc.invalidation.util.ProtoWrapper.ValidationException;
42 import android.app.IntentService;
43 import android.content.Intent;
45 import java.util.Collection;
49 * An {@link IntentService} that manages a single Ticl.
51 * Concurrency model: {@link IntentService} guarantees that calls to {@link #onHandleIntent} will
52 * be executed serially on a dedicated thread. They may perform blocking work without blocking
53 * the application calling the service.
55 * This thread will be used as the internal-scheduler thread for the Ticl.
58 public class TiclService extends IntentService {
59 /** This class must be public so that Android can instantiate it as a service. */
61 /** Resources for the created Ticls. */
62 private AndroidResources resources;
64 /** The function for computing persistence state digests when rewriting them. */
65 private final DigestFunction digestFn = new ObjectIdDigestUtils.Sha1DigestFunction();
67 public TiclService() {
70 // If the process dies during a call to onHandleIntent, redeliver the intent when the service
72 setIntentRedelivery(true);
76 * Returns the resources to use for a Ticl. Normally, we use a new resources instance
77 * for every call, but for existing tests, we need to be able to override this function
78 * and return the same instance each time.
80 AndroidResources createResources() {
81 return ResourcesFactory.createResources(this, new AndroidClock.SystemClock(), "TiclService");
85 protected void onHandleIntent(Intent intent) {
86 // TODO: We may want to use wakelocks to prevent the phone from sleeping
87 // before we have finished handling the Intent.
93 // We create resources anew each time.
94 resources = createResources();
96 resources.getLogger().fine("onHandleIntent(%s)", intent);
99 // Dispatch the appropriate handler function based on which extra key is set.
100 if (intent.hasExtra(ProtocolIntents.CLIENT_DOWNCALL_KEY)) {
101 handleClientDowncall(intent.getByteArrayExtra(ProtocolIntents.CLIENT_DOWNCALL_KEY));
102 } else if (intent.hasExtra(ProtocolIntents.INTERNAL_DOWNCALL_KEY)) {
103 handleInternalDowncall(intent.getByteArrayExtra(ProtocolIntents.INTERNAL_DOWNCALL_KEY));
104 } else if (intent.hasExtra(ProtocolIntents.SCHEDULER_KEY)) {
105 handleSchedulerEvent(intent.getByteArrayExtra(ProtocolIntents.SCHEDULER_KEY));
107 resources.getLogger().warning("Received Intent without any recognized extras: %s", intent);
110 // Null out resources to prevent accidentally using them in the future before they have been
111 // properly re-created.
117 /** Handles a request to call a function on the ticl. */
118 private void handleClientDowncall(byte[] clientDowncallBytes) {
119 // Parse and validate the request.
120 final ClientDowncall downcall;
122 downcall = ClientDowncall.parseFrom(clientDowncallBytes);
123 } catch (ValidationException exception) {
124 resources.getLogger().warning("Failed parsing ClientDowncall from %s: %s",
125 Bytes.toLazyCompactString(clientDowncallBytes), exception.getMessage());
129 resources.getLogger().fine("Handle client downcall: %s", downcall);
131 // Restore the appropriate Ticl.
132 // TODO: what if this is the "wrong" Ticl?
133 AndroidInvalidationClientImpl ticl = loadExistingTicl();
135 resources.getLogger().warning("Dropping client downcall since no Ticl: %s", downcall);
139 // Call the appropriate method.
140 if (downcall.getNullableAck() != null) {
142 AckHandle.newInstance(downcall.getNullableAck().getAckHandle().getByteArray()));
143 } else if (downcall.hasStart()) {
145 } else if (downcall.hasStop()) {
147 } else if (downcall.getNullableRegistrations() != null) {
148 RegistrationDowncall regDowncall = downcall.getNullableRegistrations();
149 if (!regDowncall.getRegistrations().isEmpty()) {
150 Collection<ObjectId> objects =
151 ProtoWrapperConverter.convertFromObjectIdProtoCollection(regDowncall.getRegistrations());
152 ticl.register(objects);
154 if (!regDowncall.getUnregistrations().isEmpty()) {
155 Collection<ObjectId> objects = ProtoWrapperConverter.convertFromObjectIdProtoCollection(
156 regDowncall.getUnregistrations());
157 ticl.unregister(objects);
160 throw new RuntimeException("Invalid downcall passed validation: " + downcall);
162 // If we are stopping the Ticl, then just delete its persisted in-memory state, since no
163 // operations on a stopped Ticl are valid. Otherwise, save the Ticl in-memory state to
165 if (downcall.hasStop()) {
166 TiclStateManager.deleteStateFile(this);
168 TiclStateManager.saveTicl(this, resources.getLogger(), ticl);
172 /** Handles an internal downcall on the Ticl. */
173 private void handleInternalDowncall(byte[] internalDowncallBytes) {
174 // Parse and validate the request.
175 final InternalDowncall downcall;
177 downcall = InternalDowncall.parseFrom(internalDowncallBytes);
178 } catch (ValidationException exception) {
179 resources.getLogger().warning("Failed parsing InternalDowncall from %s: %s",
180 Bytes.toLazyCompactString(internalDowncallBytes),
181 exception.getMessage());
184 resources.getLogger().fine("Handle internal downcall: %s", downcall);
186 // Message from the data center; just forward it to the Ticl.
187 if (downcall.getNullableServerMessage() != null) {
188 // We deliver the message regardless of whether the Ticl existed, since we'll want to
189 // rewrite persistent state in the case where it did not.
190 // TODO: what if this is the "wrong" Ticl?
191 AndroidInvalidationClientImpl ticl = TiclStateManager.restoreTicl(this, resources);
192 handleServerMessage((ticl != null),
193 downcall.getNullableServerMessage().getData().getByteArray());
195 TiclStateManager.saveTicl(this, resources.getLogger(), ticl);
200 // Network online/offline status change; just forward it to the Ticl.
201 if (downcall.getNullableNetworkStatus() != null) {
202 // Network status changes only make sense for Ticls that do exist.
203 // TODO: what if this is the "wrong" Ticl?
204 AndroidInvalidationClientImpl ticl = TiclStateManager.restoreTicl(this, resources);
206 resources.getNetworkListener().onOnlineStatusChange(
207 downcall.getNullableNetworkStatus().getIsOnline());
208 TiclStateManager.saveTicl(this, resources.getLogger(), ticl);
213 // Client network address change; just forward it to the Ticl.
214 if (downcall.getNetworkAddrChange()) {
215 AndroidInvalidationClientImpl ticl = TiclStateManager.restoreTicl(this, resources);
217 resources.getNetworkListener().onAddressChange();
218 TiclStateManager.saveTicl(this, resources.getLogger(), ticl);
223 // Client creation request (meta operation).
224 if (downcall.getNullableCreateClient() != null) {
225 handleCreateClient(downcall.getNullableCreateClient());
228 throw new RuntimeException(
229 "Invalid internal downcall passed validation: " + downcall);
232 /** Handles a {@code createClient} request. */
233 private void handleCreateClient(CreateClient createClient) {
234 // Ensure no Ticl currently exists.
235 TiclStateManager.deleteStateFile(this);
237 // Create the requested Ticl.
238 resources.getLogger().fine("Create client: creating");
239 TiclStateManager.createTicl(this, resources, createClient.getClientType(),
240 createClient.getClientName().getByteArray(), createClient.getClientConfig(),
241 createClient.getSkipStartForTest());
245 * Handles a {@code message} for a {@code ticl}. If the {@code ticl} is started, delivers the
246 * message. If the {@code ticl} is not started, drops the message and clears the last message send
247 * time in the Ticl persistent storage so that the Ticl will send a heartbeat the next time it
250 private void handleServerMessage(boolean isTiclStarted, byte[] message) {
252 // Normal case -- message for a started Ticl. Deliver the message.
253 resources.getNetworkListener().onMessageReceived(message);
257 // Even if the client is stopped, attempt to send invalidations if the client is configured to
259 maybeSendBackgroundInvalidationIntent(message);
261 // The Ticl isn't started. Rewrite persistent storage so that the last-send-time is a long
262 // time ago. The next time the Ticl starts, it will send a message to the data center, which
263 // ensures that it will be marked online and that the dropped message (or an equivalent) will
265 // Android storage implementations are required to execute callbacks inline, so this code
266 // all executes synchronously.
267 resources.getLogger().fine("Message for unstarted Ticl; rewrite state");
268 resources.getStorage().readKey(InvalidationClientCore.CLIENT_TOKEN_KEY,
269 new Callback<SimplePair<Status, byte[]>>() {
271 public void accept(SimplePair<Status, byte[]> result) {
272 byte[] stateBytes = result.second;
273 if (stateBytes == null) {
274 resources.getLogger().info("No persistent state found for client; not rewriting");
277 // Create new state identical to the old state except with a cleared
278 // lastMessageSendTimeMs.
279 PersistentTiclState state = PersistenceUtils.deserializeState(
280 resources.getLogger(), stateBytes, digestFn);
282 resources.getLogger().warning("Ignoring invalid Ticl state: %s",
283 Bytes.toLazyCompactString(stateBytes));
286 PersistentTiclState.Builder stateBuilder = state.toBuilder();
287 stateBuilder.lastMessageSendTimeMs = 0L;
288 state = stateBuilder.build();
290 // Serialize the new state and write it to storage.
291 byte[] newClientState = PersistenceUtils.serializeState(state, digestFn);
292 resources.getStorage().writeKey(InvalidationClientCore.CLIENT_TOKEN_KEY, newClientState,
293 new Callback<Status>() {
295 public void accept(Status status) {
296 if (status.getCode() != Status.Code.SUCCESS) {
297 resources.getLogger().warning(
298 "Failed saving rewritten persistent state to storage");
307 * If a service is registered to handle them, forward invalidations received while the
308 * invalidation client is stopped.
310 private void maybeSendBackgroundInvalidationIntent(byte[] message) {
311 // If a service is registered to receive background invalidations, parse the message to see if
312 // any of them should be forwarded.
313 AndroidTiclManifest manifest = new AndroidTiclManifest(getApplicationContext());
314 String backgroundServiceClass =
315 manifest.getBackgroundInvalidationListenerServiceClass();
316 if (backgroundServiceClass != null) {
318 ServerToClientMessage s2cMessage = ServerToClientMessage.parseFrom(message);
319 if (s2cMessage.getNullableInvalidationMessage() != null) {
320 Intent intent = ProtocolIntents.newBackgroundInvalidationIntent(
321 s2cMessage.getNullableInvalidationMessage());
322 intent.setClassName(getApplicationContext(), backgroundServiceClass);
323 startService(intent);
325 } catch (ValidationException exception) {
326 resources.getLogger().info("Failed to parse message: %s", exception.getMessage());
331 /** Handles a request to call a particular recurring task on the Ticl. */
332 private void handleSchedulerEvent(byte[] schedulerEventBytes) {
333 // Parse and validate the request.
334 final AndroidSchedulerEvent event;
336 event = AndroidSchedulerEvent.parseFrom(schedulerEventBytes);
337 } catch (ValidationException exception) {
338 resources.getLogger().warning("Failed parsing SchedulerEvent from %s: %s",
339 Bytes.toLazyCompactString(schedulerEventBytes), exception.getMessage());
343 resources.getLogger().fine("Handle scheduler event: %s", event);
345 // Restore the appropriate Ticl.
346 AndroidInvalidationClientImpl ticl = TiclStateManager.restoreTicl(this, resources);
348 // If the Ticl didn't exist, drop the event.
350 resources.getLogger().fine("Dropping event %s; Ticl state does not exist",
351 event.getEventName());
355 // Invoke the appropriate event.
356 AndroidInternalScheduler ticlScheduler =
357 (AndroidInternalScheduler) resources.getInternalScheduler();
358 ticlScheduler.handleSchedulerEvent(event);
360 // Save the Ticl state to persistent storage.
361 TiclStateManager.saveTicl(this, resources.getLogger(), ticl);
365 * Returns the existing Ticl from persistent storage, or {@code null} if it does not exist.
366 * If it does not exist, raises an error to the listener. This function should be used
367 * only when loading a Ticl in response to a client-application call, since it raises an error
368 * back to the application.
370 private AndroidInvalidationClientImpl loadExistingTicl() {
371 AndroidInvalidationClientImpl ticl = TiclStateManager.restoreTicl(this, resources);
373 informListenerOfPermanentError("Client does not exist on downcall");
378 /** Informs the listener of a non-retryable {@code error}. */
379 private void informListenerOfPermanentError(final String error) {
380 ErrorInfo errorInfo = ErrorInfo.newInstance(0, false, error, null);
381 Intent errorIntent = ProtocolIntents.ListenerUpcalls.newErrorIntent(errorInfo);
382 IntentForwardingListener.issueIntent(this, errorIntent);
385 /** Returns the resources used for the current Ticl. */
386 AndroidResources getSystemResourcesForTest() {