diff --git a/emailcommon/src/com/android/emailcommon/utility/DelayedOperations.java b/emailcommon/src/com/android/emailcommon/utility/DelayedOperations.java new file mode 100644 index 000000000..29324a315 --- /dev/null +++ b/emailcommon/src/com/android/emailcommon/utility/DelayedOperations.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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.android.emailcommon.utility; + +import com.google.common.annotations.VisibleForTesting; + +import android.os.Handler; + +import java.util.ArrayList; +import java.util.LinkedList; + +/** + * Class that helps post {@link Runnable}s to a {@link Handler}, and cancel pending ones + * at once. + */ +public class DelayedOperations { + private final Handler mHandler; + + @VisibleForTesting + final LinkedList mPendingOperations = new LinkedList(); + + private class QueuedOperation implements Runnable { + private final Runnable mActualRannable; + + public QueuedOperation(Runnable actualRannable) { + mActualRannable = actualRannable; + } + + @Override + public void run() { + mPendingOperations.remove(this); + mActualRannable.run(); + } + + public void cancel() { + mPendingOperations.remove(this); + cancelRunnable(this); + } + } + + public DelayedOperations(Handler handler) { + mHandler = handler; + } + + /** + * Post a {@link Runnable} to the handler. Equivalent to {@link Handler#post(Runnable)}. + */ + public void post(Runnable r) { + final QueuedOperation qo = new QueuedOperation(r); + mPendingOperations.add(qo); + postRunnable(qo); + } + + /** + * Cancel a runnable that's been posted with {@link #post(Runnable)}. + * + * Equivalent to {@link Handler#removeCallbacks(Runnable)}. + */ + public void removeCallbacks(Runnable r) { + QueuedOperation found = null; + for (QueuedOperation qo : mPendingOperations) { + if (qo.mActualRannable == r) { + found = qo; + break; + } + } + if (found != null) { + found.cancel(); + } + } + + /** + * Cancel all pending {@link Runnable}s. + */ + public void removeCallbacks() { + // To avoid ConcurrentModificationException + final ArrayList temp = new ArrayList(mPendingOperations); + for (QueuedOperation qo : temp) { + qo.cancel(); + } + } + + /** Overridden by test, as Handler is not mockable. */ + void postRunnable(Runnable r) { + mHandler.post(r); + } + + /** Overridden by test, as Handler is not mockable. */ + void cancelRunnable(Runnable r) { + mHandler.removeCallbacks(r); + } +} diff --git a/tests/src/com/android/emailcommon/utility/DelayedOperationsTests.java b/tests/src/com/android/emailcommon/utility/DelayedOperationsTests.java new file mode 100644 index 000000000..641ad88e8 --- /dev/null +++ b/tests/src/com/android/emailcommon/utility/DelayedOperationsTests.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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.android.emailcommon.utility; + +import android.test.AndroidTestCase; + +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +public class DelayedOperationsTests extends AndroidTestCase { + private DelayedOperationsForTest mDelayedOperations; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + mDelayedOperations = new DelayedOperationsForTest(); + } + + public void testEnueue() { + // Can pass only final vars, so AtomicInteger. + final AtomicInteger i = new AtomicInteger(1); + + mDelayedOperations.post(new Runnable() { + @Override public void run() { + i.addAndGet(2); + } + }); + + mDelayedOperations.post(new Runnable() { + @Override public void run() { + i.addAndGet(4); + } + }); + + // 2 ops queued. + assertEquals(2, mDelayedOperations.mPendingOperations.size()); + + // Value still not changed. + assertEquals(1, i.get()); + + // Execute all pending tasks! + mDelayedOperations.runQueuedOperations(); + + // 1 + 2 + 4 = 7 + assertEquals(7, i.get()); + + // No pending tasks. + assertEquals(0, mDelayedOperations.mPendingOperations.size()); + } + + public void testCancel() { + // Can pass only final vars, so AtomicInteger. + final AtomicInteger i = new AtomicInteger(1); + + // Post & cancel it immediately + Runnable r; + mDelayedOperations.post(r = new Runnable() { + @Override public void run() { + i.addAndGet(2); + } + }); + mDelayedOperations.removeCallbacks(r); + + mDelayedOperations.post(new Runnable() { + @Override public void run() { + i.addAndGet(4); + } + }); + + // 1 op queued. + assertEquals(1, mDelayedOperations.mPendingOperations.size()); + + // Value still not changed. + assertEquals(1, i.get()); + + // Execute all pending tasks! + mDelayedOperations.runQueuedOperations(); + + // 1 + 4 = 5 + assertEquals(5, i.get()); + + // No pending tasks. + assertEquals(0, mDelayedOperations.mPendingOperations.size()); + } + + public void testCancelAll() { + // Can pass only final vars, so AtomicInteger. + final AtomicInteger i = new AtomicInteger(1); + + mDelayedOperations.post(new Runnable() { + @Override public void run() { + i.addAndGet(2); + } + }); + + mDelayedOperations.post(new Runnable() { + @Override public void run() { + i.addAndGet(4); + } + }); + + // 2 op queued. + assertEquals(2, mDelayedOperations.mPendingOperations.size()); + + // Value still not changed. + assertEquals(1, i.get()); + + // Cancel all!! + mDelayedOperations.removeCallbacks(); + + // There should be no pending tasks in handler. + assertEquals(0, mDelayedOperations.mPostedToHandler.size()); + + // Nothing should have changed. + assertEquals(1, i.get()); + + // No pending tasks. + assertEquals(0, mDelayedOperations.mPendingOperations.size()); + } + + private static class DelayedOperationsForTest extends DelayedOperations { + // Represents all runnables pending in the handler. + public final ArrayList mPostedToHandler = new ArrayList(); + + public DelayedOperationsForTest() { + super(null); + } + + // Emulate Handler.post + @Override + void postRunnable(Runnable r) { + mPostedToHandler.add(r); + } + + // Emulate Handler.removeCallbacks + @Override + void cancelRunnable(Runnable r) { + mPostedToHandler.remove(r); + } + + public void runQueuedOperations() { + for (Runnable r : mPostedToHandler) { + r.run(); + } + mPostedToHandler.clear(); + } + } +}