/* * Copyright 2011 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.ipc.invalidation.ticl.android2; import com.google.ipc.invalidation.common.DigestFunction; import com.google.ipc.invalidation.common.ObjectIdDigestUtils; import com.google.ipc.invalidation.external.client.types.AckHandle; import com.google.ipc.invalidation.external.client.types.Callback; import com.google.ipc.invalidation.external.client.types.ErrorInfo; import com.google.ipc.invalidation.external.client.types.ObjectId; import com.google.ipc.invalidation.external.client.types.SimplePair; import com.google.ipc.invalidation.external.client.types.Status; import com.google.ipc.invalidation.ticl.InvalidationClientCore; import com.google.ipc.invalidation.ticl.PersistenceUtils; import com.google.ipc.invalidation.ticl.ProtoConverter; import com.google.ipc.invalidation.ticl.android2.AndroidInvalidationClientImpl.IntentForwardingListener; import com.google.ipc.invalidation.ticl.android2.ResourcesFactory.AndroidResources; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protos.ipc.invalidation.AndroidService.AndroidSchedulerEvent; import com.google.protos.ipc.invalidation.AndroidService.ClientDowncall; import com.google.protos.ipc.invalidation.AndroidService.ClientDowncall.RegistrationDowncall; import com.google.protos.ipc.invalidation.AndroidService.InternalDowncall; import com.google.protos.ipc.invalidation.AndroidService.InternalDowncall.CreateClient; import com.google.protos.ipc.invalidation.Client.PersistentTiclState; import android.app.IntentService; import android.content.Intent; import java.util.List; /** * An {@link IntentService} that manages a single Ticl. * <p> * Concurrency model: {@link IntentService} guarantees that calls to {@link #onHandleIntent} will * be executed serially on a dedicated thread. They may perform blocking work without blocking * the application calling the service. * <p> * This thread will be used as the internal-scheduler thread for the Ticl. * */ public class TiclService extends IntentService { /** This class must be public so that Android can instantiate it as a service. */ /** Resources for the created Ticls. */ private AndroidResources resources; /** Validator for received messages. */ private AndroidIntentProtocolValidator validator; /** The function for computing persistence state digests when rewriting them. */ private final DigestFunction digestFn = new ObjectIdDigestUtils.Sha1DigestFunction(); public TiclService() { super("TiclService"); // If the process dies during a call to onHandleIntent, redeliver the intent when the service // restarts. setIntentRedelivery(true); } /** * Returns the resources to use for a Ticl. Normally, we use a new resources instance * for every call, but for existing tests, we need to be able to override this function * and return the same instance each time. */ AndroidResources createResources() { return ResourcesFactory.createResources(this, new AndroidClock.SystemClock(), "TiclService"); } @Override protected void onHandleIntent(Intent intent) { // TODO: We may want to use wakelocks to prevent the phone from sleeping // before we have finished handling the Intent. // We create resources anew each time. resources = createResources(); resources.start(); resources.getLogger().fine("onHandleIntent(%s)", AndroidStrings.toLazyCompactString(intent)); validator = new AndroidIntentProtocolValidator(resources.getLogger()); try { if (intent == null) { resources.getLogger().fine("Ignoring null intent"); return; } // Dispatch the appropriate handler function based on which extra key is set. if (intent.hasExtra(ProtocolIntents.CLIENT_DOWNCALL_KEY)) { handleClientDowncall(intent.getByteArrayExtra(ProtocolIntents.CLIENT_DOWNCALL_KEY)); } else if (intent.hasExtra(ProtocolIntents.INTERNAL_DOWNCALL_KEY)) { handleInternalDowncall(intent.getByteArrayExtra(ProtocolIntents.INTERNAL_DOWNCALL_KEY)); } else if (intent.hasExtra(ProtocolIntents.SCHEDULER_KEY)) { handleSchedulerEvent(intent.getByteArrayExtra(ProtocolIntents.SCHEDULER_KEY)); } else { resources.getLogger().warning("Received Intent without any recognized extras: %s", intent); } } finally { // Null out resources and validator to prevent accidentally using them in the future before // they have been properly re-created. resources.stop(); resources = null; validator = null; } } /** Handles a request to call a function on the ticl. */ private void handleClientDowncall(byte[] clientDowncallBytes) { // Parse the request. final ClientDowncall downcall; try { downcall = ClientDowncall.parseFrom(clientDowncallBytes); } catch (InvalidProtocolBufferException exception) { resources.getLogger().warning("Failed parsing ClientDowncall from %s: %s", clientDowncallBytes, exception.getMessage()); return; } // Validate the request. if (!validator.isDowncallValid(downcall)) { resources.getLogger().warning("Ignoring invalid downcall message: %s", downcall); return; } resources.getLogger().fine("Handle client downcall: %s", downcall); // Restore the appropriate Ticl. // TODO: what if this is the "wrong" Ticl? AndroidInvalidationClientImpl ticl = loadExistingTicl(); if (ticl == null) { resources.getLogger().warning("Dropping client downcall since no Ticl: %s", downcall); return; } // Call the appropriate method. if (downcall.hasAck()) { ticl.acknowledge(AckHandle.newInstance(downcall.getAck().getAckHandle().toByteArray())); } else if (downcall.hasStart()) { ticl.start(); } else if (downcall.hasStop()) { ticl.stop(); } else if (downcall.hasRegistrations()) { RegistrationDowncall regDowncall = downcall.getRegistrations(); if (regDowncall.getRegistrationsCount() > 0) { List<ObjectId> objects = ProtoConverter.convertToObjectIdList( regDowncall.getRegistrationsList()); ticl.register(objects); } if (regDowncall.getUnregistrationsCount() > 0) { List<ObjectId> objects = ProtoConverter.convertToObjectIdList( regDowncall.getUnregistrationsList()); ticl.unregister(objects); } } else { throw new RuntimeException("Invalid downcall passed validation: " + downcall); } // If we are stopping the Ticl, then just delete its persisted in-memory state, since no // operations on a stopped Ticl are valid. Otherwise, save the Ticl in-memory state to // stable storage. if (downcall.hasStop()) { TiclStateManager.deleteStateFile(this); } else { TiclStateManager.saveTicl(this, resources.getLogger(), ticl); } } /** Handles an internal downcall on the Ticl. */ private void handleInternalDowncall(byte[] internalDowncallBytes) { // Parse the request. final InternalDowncall downcall; try { downcall = InternalDowncall.parseFrom(internalDowncallBytes); } catch (InvalidProtocolBufferException exception) { resources.getLogger().warning("Failed parsing InternalDowncall from %s: %s", internalDowncallBytes, exception.getMessage()); return; } // Validate the request. if (!validator.isInternalDowncallValid(downcall)) { resources.getLogger().warning("Ignoring invalid internal downcall message: %s", downcall); return; } resources.getLogger().fine("Handle internal downcall: %s", downcall); // Message from the data center; just forward it to the Ticl. if (downcall.hasServerMessage()) { // We deliver the message regardless of whether the Ticl existed, since we'll want to // rewrite persistent state in the case where it did not. // TODO: what if this is the "wrong" Ticl? AndroidInvalidationClientImpl ticl = TiclStateManager.restoreTicl(this, resources); handleServerMessage((ticl != null), downcall.getServerMessage().getData().toByteArray()); if (ticl != null) { TiclStateManager.saveTicl(this, resources.getLogger(), ticl); } return; } // Network online/offline status change; just forward it to the Ticl. if (downcall.hasNetworkStatus()) { // Network status changes only make sense for Ticls that do exist. // TODO: what if this is the "wrong" Ticl? AndroidInvalidationClientImpl ticl = TiclStateManager.restoreTicl(this, resources); if (ticl != null) { resources.getNetworkListener().onOnlineStatusChange( downcall.getNetworkStatus().getIsOnline()); TiclStateManager.saveTicl(this, resources.getLogger(), ticl); } return; } // Client network address change; just forward it to the Ticl. if (downcall.getNetworkAddrChange()) { AndroidInvalidationClientImpl ticl = TiclStateManager.restoreTicl(this, resources); if (ticl != null) { resources.getNetworkListener().onAddressChange(); TiclStateManager.saveTicl(this, resources.getLogger(), ticl); } return; } // Client creation request (meta operation). if (downcall.hasCreateClient()) { handleCreateClient(downcall.getCreateClient()); return; } throw new RuntimeException("Invalid internal downcall passed validation: " + downcall); } /** Handles a {@code createClient} request. */ private void handleCreateClient(CreateClient createClient) { // Ensure no Ticl currently exists. TiclStateManager.deleteStateFile(this); // Create the requested Ticl. resources.getLogger().fine("Create client: creating"); TiclStateManager.createTicl(this, resources, createClient.getClientType(), createClient.getClientName().toByteArray(), createClient.getClientConfig(), createClient.getSkipStartForTest()); } /** * Handles a {@code message} for a {@code ticl}. If the {@code ticl} is started, delivers the * message. If the {@code ticl} is not started, drops the message and clears the last message send * time in the Ticl persistent storage so that the Ticl will send a heartbeat the next time it * starts. */ private void handleServerMessage(boolean isTiclStarted, byte[] message) { if (isTiclStarted) { // Normal case -- message for a started Ticl. Deliver the message. resources.getNetworkListener().onMessageReceived(message); return; } // The Ticl isn't started. Rewrite persistent storage so that the last-send-time is a long // time ago. The next time the Ticl starts, it will send a message to the data center, which // ensures that it will be marked online and that the dropped message (or an equivalent) will // be delivered. // Android storage implementations are required to execute callbacks inline, so this code // all executes synchronously. resources.getLogger().fine("Message for unstarted Ticl; rewrite state"); resources.getStorage().readKey(InvalidationClientCore.CLIENT_TOKEN_KEY, new Callback<SimplePair<Status, byte[]>>() { @Override public void accept(SimplePair<Status, byte[]> result) { byte[] stateBytes = result.second; if (stateBytes == null) { resources.getLogger().info("No persistent state found for client; not rewriting"); return; } // Create new state identical to the old state except with a cleared // lastMessageSendTimeMs. PersistentTiclState state = PersistenceUtils.deserializeState( resources.getLogger(), stateBytes, digestFn); if (state == null) { resources.getLogger().warning("Ignoring invalid Ticl state: %s", stateBytes); return; } PersistentTiclState newState = PersistentTiclState.newBuilder(state) .setLastMessageSendTimeMs(0) .build(); // Serialize the new state and write it to storage. byte[] newClientState = PersistenceUtils.serializeState(newState, digestFn); resources.getStorage().writeKey(InvalidationClientCore.CLIENT_TOKEN_KEY, newClientState, new Callback<Status>() { @Override public void accept(Status status) { if (status.getCode() != Status.Code.SUCCESS) { resources.getLogger().warning( "Failed saving rewritten persistent state to storage"); } } }); } }); } /** Handles a request to call a particular recurring task on the Ticl. */ private void handleSchedulerEvent(byte[] schedulerEventBytes) { // Parse the request. final AndroidSchedulerEvent event; try { event = AndroidSchedulerEvent.parseFrom(schedulerEventBytes); } catch (InvalidProtocolBufferException exception) { resources.getLogger().warning("Failed parsing SchedulerEvent from %s: %s", schedulerEventBytes, exception.getMessage()); return; } // Validate the request. if (!validator.isSchedulerEventValid(event)) { resources.getLogger().warning("Ignoring invalid scheduler event: %s", event); return; } resources.getLogger().fine("Handle scheduler event: %s", event); // Restore the appropriate Ticl. AndroidInvalidationClientImpl ticl = TiclStateManager.restoreTicl(this, resources); // If the Ticl didn't exist, drop the event. if (ticl == null) { resources.getLogger().fine("Dropping event %s; Ticl state does not exist", event.getEventName()); return; } // Invoke the appropriate event. AndroidInternalScheduler ticlScheduler = (AndroidInternalScheduler) resources.getInternalScheduler(); ticlScheduler.handleSchedulerEvent(event); // Save the Ticl state to persistent storage. TiclStateManager.saveTicl(this, resources.getLogger(), ticl); } /** * Returns the existing Ticl from persistent storage, or {@code null} if it does not exist. * If it does not exist, raises an error to the listener. This function should be used * only when loading a Ticl in response to a client-application call, since it raises an error * back to the application. */ private AndroidInvalidationClientImpl loadExistingTicl() { AndroidInvalidationClientImpl ticl = TiclStateManager.restoreTicl(this, resources); if (ticl == null) { informListenerOfPermanentError("Client does not exist on downcall"); } return ticl; } /** Informs the listener of a non-retryable {@code error}. */ private void informListenerOfPermanentError(final String error) { ErrorInfo errorInfo = ErrorInfo.newInstance(0, false, error, null); Intent errorIntent = ProtocolIntents.ListenerUpcalls.newErrorIntent(errorInfo); IntentForwardingListener.issueIntent(this, errorIntent); } /** Returns the resources used for the current Ticl. */ AndroidResources getSystemResourcesForTest() { return resources; } }