/* * 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.SystemResources; import com.google.ipc.invalidation.external.client.SystemResources.Logger; import com.google.ipc.invalidation.external.client.SystemResources.Scheduler; import com.google.ipc.invalidation.ticl.RecurringTask; import com.google.ipc.invalidation.util.NamedRunnable; import com.google.ipc.invalidation.util.TypedUtil; import com.google.protos.ipc.invalidation.AndroidService.AndroidSchedulerEvent; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import java.util.HashMap; import java.util.Map; /** * Scheduler for controlling access to internal Ticl state in Android. * <p> * This class maintains a map from recurring task names to the recurring task instances in the * associated Ticl. To schedule a recurring task, it uses the {@link AlarmManager} to schedule * an intent to itself at the appropriate time in the future. This intent contains the name of * the task to run; when it is received, this class looks up the appropriate recurring task * instance and runs it. * <p> * Note that this class only supports scheduling recurring tasks, not ordinary runnables. In * order for it to be used, the application must declare the AlarmReceiver of the scheduler * in the application's manifest file; see the implementation comment in AlarmReceiver for * details. * */ public final class AndroidInternalScheduler implements Scheduler { /** Class that receives AlarmManager broadcasts and reissues them as intents for this service. */ public static final class AlarmReceiver extends BroadcastReceiver { /* * This class needs to be public so that it can be instantiated by the Android runtime. * Additionally, it should be declared as a broadcast receiver in the application manifest: * <receiver android:name="com.google.ipc.invalidation.ticl.android2.\ * AndroidInternalScheduler$AlarmReceiver" android:enabled="true"/> */ @Override public void onReceive(Context context, Intent intent) { // Resend the intent to the service so that it's processed on the handler thread and with // the automatic shutdown logic provided by IntentService. intent.setClassName(context, new AndroidTiclManifest(context).getTiclServiceClass()); context.startService(intent); } } /** * If {@code true}, {@link #isRunningOnThread} will verify that calls are being made from either * the {@link TiclService} or the {@link TestableTiclService.TestableClient}. */ public static boolean checkStackForTest = false; /** Class name of the testable client class, for checking call stacks in tests. */ private static final String TESTABLE_CLIENT_CLASSNAME_FOR_TEST = "com.google.ipc.invalidation.ticl.android2.TestableTiclService$TestableClient"; /** * {@link RecurringTask}-created runnables that can be executed by this instance, by their names. */ private final Map<String, Runnable> registeredTasks = new HashMap<String, Runnable>(); /** Android system context. */ private final Context context; /** Source of time for computing scheduling delays. */ private final AndroidClock clock; private Logger logger; /** Id of the Ticl for which this scheduler will process events. */ private long ticlId = -1; AndroidInternalScheduler(Context context, AndroidClock clock) { this.context = Preconditions.checkNotNull(context); this.clock = Preconditions.checkNotNull(clock); } @Override public void setSystemResources(SystemResources resources) { this.logger = Preconditions.checkNotNull(resources.getLogger()); } @Override public void schedule(int delayMs, Runnable runnable) { if (!(runnable instanceof NamedRunnable)) { throw new RuntimeException("Unsupported: can only schedule named runnables, not " + runnable); } // Create an intent that will cause the service to run the right recurring task. We explicitly // target it to our AlarmReceiver so that no other process in the system can receive it and so // that our AlarmReceiver will not be able to receive events from any other broadcaster (which // it would be if we used action-based targeting). String taskName = ((NamedRunnable) runnable).getName(); Intent eventIntent = ProtocolIntents.newSchedulerIntent(taskName, ticlId); eventIntent.setClass(context, AlarmReceiver.class); // Create a pending intent that will cause the AlarmManager to fire the above intent. PendingIntent sender = PendingIntent.getBroadcast(context, (int) (Integer.MAX_VALUE * Math.random()), eventIntent, PendingIntent.FLAG_ONE_SHOT); // Schedule the pending intent after the appropriate delay. AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); long executeMs = clock.nowMs() + delayMs; alarmManager.set(AlarmManager.RTC, executeMs, sender); } /** * Handles an event intent created in {@link #schedule} by running the corresponding recurring * task. * <p> * REQUIRES: a recurring task with the name in the intent be present in {@link #registeredTasks}. */ void handleSchedulerEvent(AndroidSchedulerEvent event) { Runnable recurringTaskRunnable = Preconditions.checkNotNull( TypedUtil.mapGet(registeredTasks, event.getEventName()), "No task registered for %s", event.getEventName()); if (ticlId != event.getTiclId()) { logger.warning("Ignoring event with wrong ticl id (not %s): %s", ticlId, event); return; } recurringTaskRunnable.run(); } /** * Registers {@code task} so that it can be subsequently run by the scheduler. * <p> * REQUIRES: no recurring task with the same name be already present in {@link #registeredTasks}. */ void registerTask(String name, Runnable runnable) { Runnable previous = registeredTasks.put(name, runnable); Preconditions.checkState(previous == null, "Cannot overwrite task registered on %s, %s; tasks = %s", name, this, registeredTasks.keySet()); } @Override public boolean isRunningOnThread() { if (!checkStackForTest) { return true; } // If requested, check that the current stack looks legitimate. for (StackTraceElement stackElement : Thread.currentThread().getStackTrace()) { if (stackElement.getMethodName().equals("onHandleIntent") && stackElement.getClassName().contains("TiclService")) { // Called from the TiclService. return true; } if (stackElement.getClassName().equals(TESTABLE_CLIENT_CLASSNAME_FOR_TEST)) { // Called from the TestableClient. return true; } } return false; } @Override public long getCurrentTimeMs() { return clock.nowMs(); } /** Removes the registered tasks. */ void reset() { logger.fine("Clearing registered tasks on %s", this); registeredTasks.clear(); } /** * Sets the id of the ticl for which this scheduler will process events. We do not know the * Ticl id until done constructing the Ticl, and we need the scheduler to construct a Ticl. This * method breaks what would otherwise be a dependency cycle on getting the Ticl id. */ void setTiclId(long ticlId) { this.ticlId = ticlId; } }