From 7aebabb8d5031a89f70a11747c5860d35267c3e1 Mon Sep 17 00:00:00 2001 From: serso Date: Mon, 24 Jul 2017 16:29:37 +0200 Subject: [PATCH] Make it possible to cancel previous calculations --- .../android/calculator/Calculator.java | 53 +++++--- .../android/calculator/TaskExecutor.java | 121 ++++++++++++++++++ .../calculator/BaseCalculatorTest.java | 15 ++- 3 files changed, 163 insertions(+), 26 deletions(-) create mode 100644 app/src/main/java/org/solovyev/android/calculator/TaskExecutor.java diff --git a/app/src/main/java/org/solovyev/android/calculator/Calculator.java b/app/src/main/java/org/solovyev/android/calculator/Calculator.java index 4ca7f32a..f9253dd4 100644 --- a/app/src/main/java/org/solovyev/android/calculator/Calculator.java +++ b/app/src/main/java/org/solovyev/android/calculator/Calculator.java @@ -24,19 +24,19 @@ package org.solovyev.android.calculator; import android.content.SharedPreferences; import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.Log; + import com.squareup.otto.Bus; import com.squareup.otto.Subscribe; -import jscl.JsclArithmeticException; -import jscl.MathEngine; -import jscl.NumeralBase; -import jscl.math.Generic; -import jscl.math.function.Constants; -import jscl.math.function.IConstant; -import jscl.text.ParseInterruptedException; + import org.solovyev.android.Check; -import org.solovyev.android.calculator.calculations.*; +import org.solovyev.android.calculator.calculations.CalculationCancelledEvent; +import org.solovyev.android.calculator.calculations.CalculationFailedEvent; +import org.solovyev.android.calculator.calculations.CalculationFinishedEvent; +import org.solovyev.android.calculator.calculations.ConversionFailedEvent; +import org.solovyev.android.calculator.calculations.ConversionFinishedEvent; import org.solovyev.android.calculator.functions.FunctionsRegistry; import org.solovyev.android.calculator.jscl.JsclOperation; import org.solovyev.android.calculator.variables.CppVariable; @@ -45,12 +45,6 @@ import org.solovyev.common.msg.Message; import org.solovyev.common.msg.MessageRegistry; import org.solovyev.common.msg.MessageType; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; -import javax.measure.converter.ConversionException; import java.math.BigInteger; import java.util.ArrayList; import java.util.Collections; @@ -58,6 +52,20 @@ import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicLong; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.measure.converter.ConversionException; + +import jscl.JsclArithmeticException; +import jscl.MathEngine; +import jscl.NumeralBase; +import jscl.math.Generic; +import jscl.math.function.Constants; +import jscl.math.function.IConstant; +import jscl.text.ParseInterruptedException; + @Singleton public class Calculator implements SharedPreferences.OnSharedPreferenceChangeListener { @@ -70,7 +78,7 @@ public class Calculator implements SharedPreferences.OnSharedPreferenceChangeLis @Nonnull final Bus bus; @Nonnull - private final Executor background; + private final TaskExecutor executor = new TaskExecutor(); private volatile boolean calculateOnFly = true; @@ -82,14 +90,17 @@ public class Calculator implements SharedPreferences.OnSharedPreferenceChangeLis ToJsclTextProcessor preprocessor; @Inject - public Calculator(@Nonnull SharedPreferences preferences, @Nonnull Bus bus, @Named(AppModule.THREAD_BACKGROUND) @Nonnull Executor background) { + public Calculator(@Nonnull SharedPreferences preferences, @Nonnull Bus bus) { this.preferences = preferences; this.bus = bus; - this.background = background; bus.register(this); preferences.registerOnSharedPreferenceChangeListener(this); } + @VisibleForTesting + void setSynchronous() { + executor.setSynchronous(); + } @Nonnull private static String convert(@Nonnull Generic generic, @Nonnull NumeralBase to) throws ConversionException { @@ -112,12 +123,12 @@ public class Calculator implements SharedPreferences.OnSharedPreferenceChangeLis public long evaluate(@Nonnull final JsclOperation operation, @Nonnull final String expression, final long sequence) { - background.execute(new Runnable() { + executor.execute(new Runnable() { @Override public void run() { evaluateAsync(sequence, operation, expression); } - }); + }, true); return sequence; } @@ -235,7 +246,7 @@ public class Calculator implements SharedPreferences.OnSharedPreferenceChangeLis return; } - background.execute(new Runnable() { + executor.execute(new Runnable() { @Override public void run() { try { @@ -245,7 +256,7 @@ public class Calculator implements SharedPreferences.OnSharedPreferenceChangeLis bus.post(new ConversionFailedEvent(state)); } } - }); + }, false); } public boolean canConvert(@Nonnull Generic generic, @NonNull NumeralBase from, @Nonnull NumeralBase to) { diff --git a/app/src/main/java/org/solovyev/android/calculator/TaskExecutor.java b/app/src/main/java/org/solovyev/android/calculator/TaskExecutor.java new file mode 100644 index 00000000..1a4a2b19 --- /dev/null +++ b/app/src/main/java/org/solovyev/android/calculator/TaskExecutor.java @@ -0,0 +1,121 @@ +package org.solovyev.android.calculator; + +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; +import android.util.Log; + +import org.solovyev.android.Check; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.annotation.Nonnull; + +class TaskExecutor { + + private class Task implements Runnable { + + @NonNull + private final Runnable runnable; + private final boolean cancellable; + @NonNull + private final Future future; + + private Task(@NonNull Runnable runnable, boolean cancellable) { + this.runnable = runnable; + this.cancellable = cancellable; + this.future = executor.submit(this); + } + + @Override + public void run() { + Log.d(TAG, "Running task: " + System.identityHashCode(this) + " on " + + Thread.currentThread().getName()); + try { + runnable.run(); + } finally { + onTaskFinished(this); + } + } + + boolean isFinished() { + return future.isDone() || future.isCancelled(); + } + + void cancel() { + Log.d(TAG, "Task cancelled: " + System.identityHashCode(this)); + Check.isTrue(cancellable); + future.cancel(true); + } + } + + private static final int MAX_TASKS = 5; + @NonNull + private static final String TAG = "TaskExecutor"; + @NonNull + private final List tasks = new ArrayList<>(); + @NonNull + private final ExecutorService executor = makeExecutor(); + private boolean synchronous = false; + + @NonNull + private static ExecutorService makeExecutor() { + return Executors.newCachedThreadPool( + new ThreadFactory() { + @NonNull + private final AtomicInteger counter = new AtomicInteger(); + + @Override + public Thread newThread(@Nonnull Runnable r) { + return new Thread(r, "Task #" + counter.getAndIncrement()); + } + }); + } + + void execute(@NonNull Runnable runnable, boolean cancellable) { + Check.isMainThread(); + if (synchronous) { + runnable.run(); + return; + } + synchronized (tasks) { + if (tasks.size() >= MAX_TASKS) { + for (int i = 0; i < tasks.size(); i++) { + final Task task = tasks.get(i); + if (task.cancellable) { + tasks.remove(i); + task.cancel(); + break; + } + } + } + } + onTaskStarted(new Task(runnable, cancellable)); + } + + private void onTaskStarted(@NonNull Task task) { + synchronized (tasks) { + if (!task.isFinished()) { + Log.d(TAG, "Task added: " + System.identityHashCode(task)); + tasks.add(task); + } + } + } + + private void onTaskFinished(@NonNull Task task) { + synchronized (tasks) { + Log.d(TAG, "Task removed: " + System.identityHashCode(task)); + tasks.remove(task); + } + } + + @VisibleForTesting + void setSynchronous() { + synchronous = true; + } +} \ No newline at end of file diff --git a/app/src/test/java/org/solovyev/android/calculator/BaseCalculatorTest.java b/app/src/test/java/org/solovyev/android/calculator/BaseCalculatorTest.java index 93cdbdbb..ba00c70b 100644 --- a/app/src/test/java/org/solovyev/android/calculator/BaseCalculatorTest.java +++ b/app/src/test/java/org/solovyev/android/calculator/BaseCalculatorTest.java @@ -1,8 +1,16 @@ package org.solovyev.android.calculator; +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.solovyev.android.calculator.jscl.JsclOperation.numeric; + import android.content.SharedPreferences; import android.support.annotation.NonNull; + import com.squareup.otto.Bus; + import org.hamcrest.Description; import org.junit.Before; import org.mockito.ArgumentMatcher; @@ -10,10 +18,6 @@ import org.solovyev.android.calculator.calculations.CalculationFailedEvent; import org.solovyev.android.calculator.calculations.CalculationFinishedEvent; import org.solovyev.android.calculator.jscl.JsclOperation; -import static org.mockito.Matchers.argThat; -import static org.mockito.Mockito.*; -import static org.solovyev.android.calculator.jscl.JsclOperation.numeric; - public abstract class BaseCalculatorTest { protected Calculator calculator; protected Bus bus; @@ -22,7 +26,8 @@ public abstract class BaseCalculatorTest { @Before public void setUp() throws Exception { bus = mock(Bus.class); - calculator = new Calculator(mock(SharedPreferences.class), bus, Tests.sameThreadExecutor()); + calculator = new Calculator(mock(SharedPreferences.class), bus); + calculator.setSynchronous(); engine = Tests.makeEngine(); engine.variablesRegistry.bus = bus; calculator.engine = engine;