blob: a6cecb1a0d3ee0c1bc4dbf4b2684e85a72126976 [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.external.client.InvalidationClient;
import com.google.ipc.invalidation.external.client.InvalidationListener;
import com.google.ipc.invalidation.external.client.SystemResources;
import com.google.ipc.invalidation.external.client.SystemResources.Logger;
import com.google.ipc.invalidation.external.client.types.AckHandle;
import com.google.ipc.invalidation.external.client.types.ErrorInfo;
import com.google.ipc.invalidation.external.client.types.Invalidation;
import com.google.ipc.invalidation.external.client.types.ObjectId;
import com.google.ipc.invalidation.ticl.InvalidationClientCore;
import com.google.ipc.invalidation.ticl.ProtoWrapperConverter;
import com.google.ipc.invalidation.ticl.android2.ProtocolIntents.ListenerUpcalls;
import com.google.ipc.invalidation.ticl.proto.AndroidService.AndroidTiclState;
import com.google.ipc.invalidation.ticl.proto.Client.AckHandleP;
import com.google.ipc.invalidation.ticl.proto.ClientProtocol.ApplicationClientIdP;
import com.google.ipc.invalidation.ticl.proto.ClientProtocol.ClientConfigP;
import com.google.ipc.invalidation.util.Preconditions;
import com.google.ipc.invalidation.util.ProtoWrapper.ValidationException;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import java.util.Arrays;
import java.util.Map;
import java.util.Random;
/**
* Android specialization of {@link InvalidationClientCore}. Configures the internal scheduler of
* the provided resources with references to the recurring tasks in the Ticl and also provides
* an {@link InvalidationListener} instance to the Ticl that will forward upcalls to the
* actual application listener using {@link Intent}s.
* <p>
* This class requires that {@code SystemResources} {@code Storage} implementations be synchronous.
* I.e., they must invoke their callbacks inline. We require this because it is very difficult
* to handle asynchrony in an Android {@code IntentService}. Every async point requires marshalling
* the Ticl state to disk. Additionally, we must be able to resume processing where we left off;
* i.e., we must be able to (morally) save the value of the program counter. Intents, unlike Java
* callbacks, do not implicitly save the PC value, so we need to manually encode it in Intent
* data. This is extremely awkward, so we avoid asynchrony in the storage API.
*
*/
class AndroidInvalidationClientImpl extends InvalidationClientCore {
/** Class implementing the application listener stub (allows overriding default for tests). */
static Class<? extends Service> listenerServiceClassForTest = null;
/**
* {@link InvalidationListener} implementation that forwards all calls to a remote listener
* using Android intents.
*/
static class IntentForwardingListener implements InvalidationListener {
/** Android system context. */
private final Context context;
/** Logger from Ticl resources. */
private final Logger logger;
IntentForwardingListener(Context context, Logger logger) {
this.context = Preconditions.checkNotNull(context);
this.logger = Preconditions.checkNotNull(logger);
}
// All calls are implemented by marshalling the arguments to an Intent and sending the Intent
// to the application.
@Override
public void ready(InvalidationClient client) {
issueIntent(context, ListenerUpcalls.newReadyIntent());
}
@Override
public void invalidate(InvalidationClient client, Invalidation invalidation,
AckHandle ackHandle) {
try {
AckHandleP ackHandleP = AckHandleP.parseFrom(ackHandle.getHandleData());
issueIntent(context, ListenerUpcalls.newInvalidateIntent(
ProtoWrapperConverter.convertToInvalidationProto(invalidation), ackHandleP));
} catch (ValidationException exception) {
// Log and drop invalid call.
logBadAckHandle("invalidate", ackHandle);
}
}
@Override
public void invalidateUnknownVersion(InvalidationClient client, ObjectId objectId,
AckHandle ackHandle) {
try {
AckHandleP ackHandleP = AckHandleP.parseFrom(ackHandle.getHandleData());
issueIntent(context, ListenerUpcalls.newInvalidateUnknownIntent(
ProtoWrapperConverter.convertToObjectIdProto(objectId), ackHandleP));
} catch (ValidationException exception) {
// Log and drop invalid call.
logBadAckHandle("invalidateUnknownVersion", ackHandle);
}
}
@Override
public void invalidateAll(InvalidationClient client, AckHandle ackHandle) {
try {
AckHandleP ackHandleP = AckHandleP.parseFrom(ackHandle.getHandleData());
issueIntent(context, ListenerUpcalls.newInvalidateAllIntent(ackHandleP));
} catch (ValidationException exception) {
// Log and drop invalid call.
logBadAckHandle("invalidateAll", ackHandle);
}
}
@Override
public void informRegistrationStatus(
InvalidationClient client, ObjectId objectId, RegistrationState regState) {
Intent intent = ListenerUpcalls.newRegistrationStatusIntent(
ProtoWrapperConverter.convertToObjectIdProto(objectId),
regState == RegistrationState.REGISTERED);
issueIntent(context, intent);
}
@Override
public void informRegistrationFailure(InvalidationClient client, ObjectId objectId,
boolean isTransient, String errorMessage) {
issueIntent(context, ListenerUpcalls.newRegistrationFailureIntent(
ProtoWrapperConverter.convertToObjectIdProto(objectId), isTransient, errorMessage));
}
@Override
public void reissueRegistrations(InvalidationClient client, byte[] prefix, int prefixLength) {
issueIntent(context, ListenerUpcalls.newReissueRegistrationsIntent(prefix, prefixLength));
}
@Override
public void informError(InvalidationClient client, ErrorInfo errorInfo) {
issueIntent(context, ListenerUpcalls.newErrorIntent(errorInfo));
}
/**
* Sends {@code intent} to the real listener via the listener intent service class.
*/
static void issueIntent(Context context, Intent intent) {
intent.setClassName(context, (listenerServiceClassForTest != null) ?
listenerServiceClassForTest.getName() :
new AndroidTiclManifest(context).getListenerServiceClass());
context.startService(intent);
}
/**
* Logs a warning that a listener upcall to {@code method} has been dropped because
* {@code unparseableHandle} could not be parsed.
*/
private void logBadAckHandle(String method, AckHandle unparseableHandle) {
logger.warning("Dropping call to %s; could not parse ack handle data %s",
method, Arrays.toString(unparseableHandle.getHandleData()));
}
}
/**
* Unique identifier for this Ticl. This is used to ensure that scheduler intents for other Ticls
* are not incorrectly delivered to this instance.
*/
private final long schedulingId;
/**
* Creates a fresh instance.
*
* @param context Android system context
* @param resources Ticl resources to use
* @param random random number generator for the Ticl
* @param clientType type of the Ticl
* @param clientName unique application name for the Ticl
* @param config configuration to use
*/
AndroidInvalidationClientImpl(Context context, SystemResources resources, Random random,
int clientType, byte[] clientName, ClientConfigP config) {
super(resources, random, clientType, clientName, config, getApplicationName(context),
new IntentForwardingListener(context, resources.getLogger()));
this.schedulingId = resources.getInternalScheduler().getCurrentTimeMs();
resources.getLogger().fine("Create new Ticl scheduling id: %s", schedulingId);
initializeSchedulerWithRecurringTasks();
}
/**
* Creates an instance with state restored from {@code marshalledState}. Other parameters are as
* in {@link InvalidationClientCore}.
*/
AndroidInvalidationClientImpl(Context context, SystemResources resources, Random random,
AndroidTiclState marshalledState) {
super(resources,
random,
marshalledState.getMetadata().getClientType(),
marshalledState.getMetadata().getClientName().getByteArray(),
marshalledState.getMetadata().getClientConfig(),
getApplicationName(context),
marshalledState.getTiclState(),
new IntentForwardingListener(context, resources.getLogger()));
this.schedulingId = marshalledState.getMetadata().getTiclId();
initializeSchedulerWithRecurringTasks();
}
/** Returns the name of the application using the Ticl. */
private static String getApplicationName(Context context) {
return context.getPackageName();
}
/**
* Provides the internal scheduler with references to each of the recurring tasks that can be
* executed.
*/
private void initializeSchedulerWithRecurringTasks() {
if (!(getResources().getInternalScheduler() instanceof AndroidInternalScheduler)) {
throw new IllegalStateException("Scheduler must be an AndroidInternalScheduler, not "
+ getResources().getInternalScheduler());
}
AndroidInternalScheduler scheduler =
(AndroidInternalScheduler) getResources().getInternalScheduler();
for (Map.Entry<String, Runnable> entry : getRecurringTasks().entrySet()) {
scheduler.registerTask(entry.getKey(), entry.getValue());
}
}
/** Returns the scheduling id of this Ticl. */
long getSchedulingId() {
return schedulingId;
}
// This method appears to serve no purpose, since it's just a delegation to the superclass method
// with the same access level (protected). However, protected also implies package access, so what
// this is doing is making this method visible to TiclStateManager.
@Override
protected ApplicationClientIdP getApplicationClientIdP() {
return super.getApplicationClientIdP();
}
// Similar rationale as getApplicationClientIdP.
@Override
protected ClientConfigP getConfig() {
return super.getConfig();
}
// Similar rationale as getApplicationClientIdP.
@Override
protected boolean isStarted() {
return super.isStarted();
}
}