/* * 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.common.base.Preconditions; 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.ProtoConverter; import com.google.ipc.invalidation.ticl.android2.ProtocolIntents.ListenerUpcalls; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protos.ipc.invalidation.AndroidService.AndroidTiclState; import com.google.protos.ipc.invalidation.Client.AckHandleP; import com.google.protos.ipc.invalidation.ClientProtocol.ApplicationClientIdP; import com.google.protos.ipc.invalidation.ClientProtocol.ClientConfigP; 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( ProtoConverter.convertToInvalidationProto(invalidation), ackHandleP)); } catch (InvalidProtocolBufferException 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( ProtoConverter.convertToObjectIdProto(objectId), ackHandleP)); } catch (InvalidProtocolBufferException 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 (InvalidProtocolBufferException exception) { // Log and drop invalid call. logBadAckHandle("invalidateAll", ackHandle); } } @Override public void informRegistrationStatus( InvalidationClient client, ObjectId objectId, RegistrationState regState) { Intent intent = ListenerUpcalls.newRegistrationStatusIntent( ProtoConverter.convertToObjectIdProto(objectId), regState == RegistrationState.REGISTERED); issueIntent(context, intent); } @Override public void informRegistrationFailure(InvalidationClient client, ObjectId objectId, boolean isTransient, String errorMessage) { issueIntent(context, ListenerUpcalls.newRegistrationFailureIntent( ProtoConverter.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().toByteArray(), 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() { Preconditions.checkState( getResources().getInternalScheduler() instanceof AndroidInternalScheduler, "Scheduler must be an AndroidInternalScheduler, not %s", 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(); } }