Make it possible to cancel previous calculations

This commit is contained in:
serso 2017-07-24 16:29:37 +02:00
parent e6aa39659f
commit 7aebabb8d5
3 changed files with 163 additions and 26 deletions

View File

@ -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) {

View File

@ -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<Task> 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;
}
}

View File

@ -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;