| /* |
| * 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); |
| } |
| } |
| } |