| /* |
| * 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.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.ticl.proto.AndroidService.AndroidSchedulerEvent; |
| import com.google.ipc.invalidation.util.NamedRunnable; |
| import com.google.ipc.invalidation.util.Preconditions; |
| import com.google.ipc.invalidation.util.TypedUtil; |
| |
| 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 = TypedUtil.mapGet(registeredTasks, event.getEventName()); |
| if (recurringTaskRunnable == null) { |
| throw new NullPointerException("No task registered for " + 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); |
| if (previous != null) { |
| String message = new StringBuilder() |
| .append("Cannot overwrite task registered on ") |
| .append(name) |
| .append(", ") |
| .append(this) |
| .append("; tasks = ") |
| .append(registeredTasks.keySet()) |
| .toString(); |
| throw new IllegalStateException(message); |
| } |
| } |
| |
| @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; |
| } |
| } |