/*
* 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.testing.android;
import com.google.common.base.Preconditions;
import com.google.ipc.invalidation.external.client.SystemResources.Logger;
import com.google.ipc.invalidation.external.client.android.service.AndroidLogger;
import com.google.ipc.invalidation.external.client.android.service.Event;
import com.google.ipc.invalidation.external.client.android.service.ListenerBinder;
import com.google.ipc.invalidation.external.client.android.service.ListenerService;
import com.google.ipc.invalidation.external.client.android.service.Request;
import com.google.ipc.invalidation.external.client.android.service.Request.Action;
import com.google.ipc.invalidation.external.client.android.service.Request.Parameter;
import com.google.ipc.invalidation.external.client.android.service.Response;
import com.google.ipc.invalidation.external.client.android.service.ServiceBinder.BoundWork;
import com.google.ipc.invalidation.ticl.android.AbstractInvalidationService;
import com.google.ipc.invalidation.util.TypedUtil;
import android.accounts.Account;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import junit.framework.Assert;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A stub invalidation service implementation that can be used to test the
* client library or invalidation applications. The test service will validate
* all incoming events sent by the client. It also supports the ability to store
* all incoming action intents and outgoing event intents and make them
* available for retrieval via the {@link InvalidationTest} interface.
* <p>
* The implementation of service intent handling will simply log the invocation
* and do nothing else.
*
*/
public class InvalidationTestService extends AbstractInvalidationService {
private static class ClientState {
final Account account;
final String authType;
final Intent eventIntent;
private ClientState(Account account, String authType, Intent eventIntent) {
this.account = account;
this.authType = authType;
this.eventIntent = eventIntent;
}
}
/**
* Intent that can be used to bind to the InvalidationTest service.
*/
public static final Intent TEST_INTENT = new Intent("com.google.ipc.invalidation.TEST");
/** Logger */
private static final Logger logger = AndroidLogger.forTag("InvTestService");
/** Map of currently active clients from key to {@link ClientState} */
private static Map<String, ClientState> clientMap = new HashMap<String, ClientState>();
/** {@code true} the test service should capture actions */
private static boolean captureActions;
/** The stored actions that are available for retrieval */
private static List<Bundle> actions = new ArrayList<Bundle>();
/** {@code true} if the client should capture events */
private static boolean captureEvents;
/** The stored events that are available for retrieval */
private static List<Bundle> events = new ArrayList<Bundle>();
/** Lock over all state in all instances. */
private static final Object LOCK = new Object();
/**
* InvalidationTest stub to handle calls from clients.
*/
private final InvalidationTest.Stub testBinder = new InvalidationTest.Stub() {
@Override
public void setCapture(boolean captureActions, boolean captureEvents) {
synchronized (LOCK) {
InvalidationTestService.captureActions = captureActions;
InvalidationTestService.captureEvents = captureEvents;
}
}
@Override
public Bundle[] getRequests() {
synchronized (LOCK) {
logger.fine("Reading actions from %s:%d", actions, actions.size());
Bundle[] value = new Bundle[actions.size()];
actions.toArray(value);
actions.clear();
return value;
}
}
@Override
public Bundle[] getEvents() {
synchronized (LOCK) {
Bundle[] value = new Bundle[events.size()];
events.toArray(value);
events.clear();
return value;
}
}
@Override
public void sendEvent(final Bundle eventBundle) {
synchronized (LOCK) {
// Retrive info for that target client
String clientKey = eventBundle.getString(Parameter.CLIENT);
ClientState state = clientMap.get(clientKey);
Preconditions.checkNotNull(state, "No state for %s in %s", clientKey, clientMap.keySet());
// Bind to the listener associated with the client and send the event
ListenerBinder binder = new ListenerBinder(getBaseContext(), state.eventIntent,
InvalidationTestListener.class.getName());
binder.runWhenBound(new BoundWork<ListenerService>() {
@Override
public void run(ListenerService service) {
InvalidationTestService.this.sendEvent(service, new Event(eventBundle));
}
});
// Will happen after the runWhenBound invokes the receiver. Could also be done inside
// the receiver.
binder.release();
}
}
@Override
public void reset() {
synchronized (LOCK) {
logger.info("Resetting test service");
captureActions = false;
captureEvents = false;
clientMap.clear();
actions.clear();
events.clear();
}
}
};
@Override
public void onCreate() {
synchronized (LOCK) {
logger.info("onCreate");
super.onCreate();
}
}
@Override
public void onDestroy() {
synchronized (LOCK) {
logger.info("onDestroy");
super.onDestroy();
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
synchronized (LOCK) {
logger.info("onStart");
return super.onStartCommand(intent, flags, startId);
}
}
@Override
public IBinder onBind(Intent intent) {
synchronized (LOCK) {
logger.info("onBind");
// For InvalidationService binding, delegate to the superclass
if (Request.SERVICE_INTENT.getAction().equals(intent.getAction())) {
return super.onBind(intent);
}
// Otherwise, return the test interface binder
return testBinder;
}
}
@Override
public boolean onUnbind(Intent intent) {
synchronized (LOCK) {
logger.info("onUnbind");
return super.onUnbind(intent);
}
}
@Override
protected void handleRequest(Bundle input, Bundle output) {
synchronized (LOCK) {
super.handleRequest(input, output);
if (captureActions) {
actions.add(input);
}
validateResponse(input, output);
}
}
@Override
protected void sendEvent(ListenerService listenerService, Event event) {
synchronized (LOCK) {
if (captureEvents) {
events.add(event.getBundle());
}
super.sendEvent(listenerService, event);
}
}
@Override
protected void create(Request request, Response.Builder response) {
synchronized (LOCK) {
validateRequest(request, Action.CREATE, Parameter.ACTION, Parameter.CLIENT,
Parameter.CLIENT_TYPE, Parameter.ACCOUNT, Parameter.AUTH_TYPE, Parameter.INTENT);
logger.info("Creating client %s:%s", request.getClientKey(), clientMap.keySet());
if (!TypedUtil.containsKey(clientMap, request.getClientKey())) {
// If no client exists with this key, create one.
clientMap.put(
request.getClientKey(), new ClientState(request.getAccount(), request.getAuthType(),
request.getIntent()));
} else {
// Otherwise, verify that the existing client has the same account / auth type / intent.
ClientState existingState = TypedUtil.mapGet(clientMap, request.getClientKey());
Preconditions.checkState(request.getAccount().equals(existingState.account));
Preconditions.checkState(request.getAuthType().equals(existingState.authType));
}
response.setStatus(Response.Status.SUCCESS);
}
}
@Override
protected void resume(Request request, Response.Builder response) {
synchronized (LOCK) {
validateRequest(
request, Action.RESUME, Parameter.ACTION, Parameter.CLIENT);
ClientState state = clientMap.get(request.getClientKey());
if (state != null) {
logger.info("Resuming client %s:%s", request.getClientKey(), clientMap.keySet());
response.setStatus(Response.Status.SUCCESS);
response.setAccount(state.account);
response.setAuthType(state.authType);
} else {
logger.warning("Cannot resume client %s:%s", request.getClientKey(), clientMap.keySet());
response.setStatus(Response.Status.INVALID_CLIENT);
}
}
}
@Override
protected void register(Request request, Response.Builder response) {
synchronized (LOCK) {
// Ensure that one (and only one) of the variant object id forms is used
String objectParam =
request.getBundle().containsKey(Parameter.OBJECT_ID) ?
Parameter.OBJECT_ID : Parameter.OBJECT_ID_LIST;
validateRequest(request, Action.REGISTER, Parameter.ACTION, Parameter.CLIENT, objectParam);
if (!validateClient(request)) {
response.setStatus(Response.Status.INVALID_CLIENT);
return;
}
response.setStatus(Response.Status.SUCCESS);
}
}
@Override
protected void unregister(Request request, Response.Builder response) {
synchronized (LOCK) {
// Ensure that one (and only one) of the variant object id forms is used
String objectParam =
request.getBundle().containsKey(Parameter.OBJECT_ID) ?
Parameter.OBJECT_ID :
Parameter.OBJECT_ID_LIST;
validateRequest(request, Action.UNREGISTER, Parameter.ACTION,
Parameter.CLIENT, objectParam);
if (!validateClient(request)) {
response.setStatus(Response.Status.INVALID_CLIENT);
return;
}
response.setStatus(Response.Status.SUCCESS);
}
}
@Override
protected void start(Request request, Response.Builder response) {
synchronized (LOCK) {
validateRequest(
request, Action.START, Parameter.ACTION, Parameter.CLIENT);
if (!validateClient(request)) {
response.setStatus(Response.Status.INVALID_CLIENT);
return;
}
response.setStatus(Response.Status.SUCCESS);
}
}
@Override
protected void stop(Request request, Response.Builder response) {
synchronized (LOCK) {
validateRequest(request, Action.STOP, Parameter.ACTION, Parameter.CLIENT);
if (!validateClient(request)) {
response.setStatus(Response.Status.INVALID_CLIENT);
return;
}
response.setStatus(Response.Status.SUCCESS);
}
}
@Override
protected void acknowledge(Request request, Response.Builder response) {
synchronized (LOCK) {
validateRequest(request, Action.ACKNOWLEDGE, Parameter.ACTION, Parameter.CLIENT,
Parameter.ACK_TOKEN);
if (!validateClient(request)) {
response.setStatus(Response.Status.INVALID_CLIENT);
return;
}
response.setStatus(Response.Status.SUCCESS);
}
}
@Override
protected void destroy(Request request, Response.Builder response) {
synchronized (LOCK) {
validateRequest(request, Action.DESTROY, Parameter.ACTION, Parameter.CLIENT);
if (!validateClient(request)) {
response.setStatus(Response.Status.INVALID_CLIENT);
return;
}
response.setStatus(Response.Status.SUCCESS);
}
}
/**
* Validates that the client associated with the request is one that has
* previously been created or resumed on the test service.
*/
private boolean validateClient(Request request) {
if (!clientMap.containsKey(request.getClientKey())) {
logger.warning("Client %s is not an active client: %s",
request.getClientKey(), clientMap.keySet());
return false;
}
return true;
}
/**
* Validates that the request contains exactly the set of parameters expected.
*
* @param request request to validate
* @param action expected action
* @param parameters expected parameters
*/
private void validateRequest(Request request, Action action, String... parameters) {
Assert.assertEquals(action, request.getAction());
List<String> expectedParameters = new ArrayList<String>(Arrays.asList(parameters));
Bundle requestBundle = request.getBundle();
for (String parameter : requestBundle.keySet()) {
Assert.assertTrue("Unexpected parameter: " + parameter, expectedParameters.remove(parameter));
// Validate the value
Object value = requestBundle.get(parameter);
Assert.assertNotNull(value);
}
Assert.assertTrue("Missing parameter:" + expectedParameters, expectedParameters.isEmpty());
}
/**
* Validates a response bundle being returned to a client contains valid
* success response.
*/
protected void validateResponse(Bundle input, Bundle output) {
synchronized (LOCK) {
int status = output.getInt(Response.Parameter.STATUS, Response.Status.UNKNOWN);
Assert.assertEquals("Unexpected failure for input = " + input + "; output = " + output,
Response.Status.SUCCESS, status);
String error = output.getString(Response.Parameter.ERROR);
Assert.assertNull(error);
}
}
/** Returns whether a client with key {@code clientKey} is known to the service. */
public static boolean clientExists(String clientKey) {
synchronized (LOCK) {
return TypedUtil.containsKey(clientMap, clientKey);
}
}
}