blob: ff979f158126fd7bb4b6550373c7d4768d7ee952 [file] [log] [blame]
/*
* 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.ProtoWrapperConverter;
import com.google.ipc.invalidation.ticl.android2.AndroidInvalidationClientImpl.IntentForwardingListener;
import com.google.ipc.invalidation.ticl.android2.ResourcesFactory.AndroidResources;
import com.google.ipc.invalidation.ticl.proto.AndroidService.AndroidSchedulerEvent;
import com.google.ipc.invalidation.ticl.proto.AndroidService.ClientDowncall;
import com.google.ipc.invalidation.ticl.proto.AndroidService.ClientDowncall.RegistrationDowncall;
import com.google.ipc.invalidation.ticl.proto.AndroidService.InternalDowncall;
import com.google.ipc.invalidation.ticl.proto.AndroidService.InternalDowncall.CreateClient;
import com.google.ipc.invalidation.ticl.proto.Client.PersistentTiclState;
import com.google.ipc.invalidation.ticl.proto.ClientProtocol.ServerToClientMessage;
import com.google.ipc.invalidation.util.Bytes;
import com.google.ipc.invalidation.util.ProtoWrapper.ValidationException;
import android.app.IntentService;
import android.content.Intent;
import java.util.Collection;
/**
* 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;
/** 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.
if (intent == null) {
return;
}
// We create resources anew each time.
resources = createResources();
resources.start();
resources.getLogger().fine("onHandleIntent(%s)", intent);
try {
// 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 to prevent accidentally using them in the future before they have been
// properly re-created.
resources.stop();
resources = null;
}
}
/** Handles a request to call a function on the ticl. */
private void handleClientDowncall(byte[] clientDowncallBytes) {
// Parse and validate the request.
final ClientDowncall downcall;
try {
downcall = ClientDowncall.parseFrom(clientDowncallBytes);
} catch (ValidationException exception) {
resources.getLogger().warning("Failed parsing ClientDowncall from %s: %s",
Bytes.toLazyCompactString(clientDowncallBytes), exception.getMessage());
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.getNullableAck() != null) {
ticl.acknowledge(
AckHandle.newInstance(downcall.getNullableAck().getAckHandle().getByteArray()));
} else if (downcall.hasStart()) {
ticl.start();
} else if (downcall.hasStop()) {
ticl.stop();
} else if (downcall.getNullableRegistrations() != null) {
RegistrationDowncall regDowncall = downcall.getNullableRegistrations();
if (!regDowncall.getRegistrations().isEmpty()) {
Collection<ObjectId> objects =
ProtoWrapperConverter.convertFromObjectIdProtoCollection(regDowncall.getRegistrations());
ticl.register(objects);
}
if (!regDowncall.getUnregistrations().isEmpty()) {
Collection<ObjectId> objects = ProtoWrapperConverter.convertFromObjectIdProtoCollection(
regDowncall.getUnregistrations());
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 and validate the request.
final InternalDowncall downcall;
try {
downcall = InternalDowncall.parseFrom(internalDowncallBytes);
} catch (ValidationException exception) {
resources.getLogger().warning("Failed parsing InternalDowncall from %s: %s",
Bytes.toLazyCompactString(internalDowncallBytes),
exception.getMessage());
return;
}
resources.getLogger().fine("Handle internal downcall: %s", downcall);
// Message from the data center; just forward it to the Ticl.
if (downcall.getNullableServerMessage() != null) {
// 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.getNullableServerMessage().getData().getByteArray());
if (ticl != null) {
TiclStateManager.saveTicl(this, resources.getLogger(), ticl);
}
return;
}
// Network online/offline status change; just forward it to the Ticl.
if (downcall.getNullableNetworkStatus() != null) {
// 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.getNullableNetworkStatus().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.getNullableCreateClient() != null) {
handleCreateClient(downcall.getNullableCreateClient());
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().getByteArray(), 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;
}
// Even if the client is stopped, attempt to send invalidations if the client is configured to
// receive them.
maybeSendBackgroundInvalidationIntent(message);
// 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",
Bytes.toLazyCompactString(stateBytes));
return;
}
PersistentTiclState.Builder stateBuilder = state.toBuilder();
stateBuilder.lastMessageSendTimeMs = 0L;
state = stateBuilder.build();
// Serialize the new state and write it to storage.
byte[] newClientState = PersistenceUtils.serializeState(state, 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");
}
}
});
}
});
}
/**
* If a service is registered to handle them, forward invalidations received while the
* invalidation client is stopped.
*/
private void maybeSendBackgroundInvalidationIntent(byte[] message) {
// If a service is registered to receive background invalidations, parse the message to see if
// any of them should be forwarded.
AndroidTiclManifest manifest = new AndroidTiclManifest(getApplicationContext());
String backgroundServiceClass =
manifest.getBackgroundInvalidationListenerServiceClass();
if (backgroundServiceClass != null) {
try {
ServerToClientMessage s2cMessage = ServerToClientMessage.parseFrom(message);
if (s2cMessage.getNullableInvalidationMessage() != null) {
Intent intent = ProtocolIntents.newBackgroundInvalidationIntent(
s2cMessage.getNullableInvalidationMessage());
intent.setClassName(getApplicationContext(), backgroundServiceClass);
startService(intent);
}
} catch (ValidationException exception) {
resources.getLogger().info("Failed to parse message: %s", exception.getMessage());
}
}
}
/** Handles a request to call a particular recurring task on the Ticl. */
private void handleSchedulerEvent(byte[] schedulerEventBytes) {
// Parse and validate the request.
final AndroidSchedulerEvent event;
try {
event = AndroidSchedulerEvent.parseFrom(schedulerEventBytes);
} catch (ValidationException exception) {
resources.getLogger().warning("Failed parsing SchedulerEvent from %s: %s",
Bytes.toLazyCompactString(schedulerEventBytes), exception.getMessage());
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;
}
}