diff --git a/app/src/main/java/org/solovyev/android/calculator/BaseUi.java b/app/src/main/java/org/solovyev/android/calculator/BaseUi.java index b21d4ffb..137c4915 100644 --- a/app/src/main/java/org/solovyev/android/calculator/BaseUi.java +++ b/app/src/main/java/org/solovyev/android/calculator/BaseUi.java @@ -33,21 +33,27 @@ import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.widget.TextView; + import org.solovyev.android.Views; +import org.solovyev.android.calculator.history.History; import org.solovyev.android.calculator.history.HistoryDragProcessor; import org.solovyev.android.calculator.view.AngleUnitsButton; import org.solovyev.android.calculator.view.LongClickEraser; import org.solovyev.android.calculator.view.NumeralBasesButton; import org.solovyev.android.calculator.view.ViewsCache; -import org.solovyev.android.views.dragbutton.*; +import org.solovyev.android.views.dragbutton.DirectionDragButton; +import org.solovyev.android.views.dragbutton.DragButton; +import org.solovyev.android.views.dragbutton.DragDirection; +import org.solovyev.android.views.dragbutton.DragListener; +import org.solovyev.android.views.dragbutton.SimpleDragListener; + +import java.util.ArrayList; +import java.util.List; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.inject.Inject; -import java.util.ArrayList; -import java.util.List; - import static org.solovyev.android.calculator.Preferences.Gui.Layout.simple; import static org.solovyev.android.calculator.Preferences.Gui.Layout.simple_mobile; import static org.solovyev.android.calculator.model.AndroidCalculatorEngine.Preferences.angleUnit; @@ -118,6 +124,9 @@ public abstract class BaseUi implements SharedPreferences.OnSharedPreferenceChan @Inject Editor editor; + @Inject + History history; + protected void onCreate(@Nonnull Activity activity) { ((CalculatorApplication) activity.getApplication()).getComponent().inject(this); @@ -166,7 +175,7 @@ public abstract class BaseUi implements SharedPreferences.OnSharedPreferenceChan final ViewsCache views = ViewsCache.forView(root); setOnDragListeners(views, activity); - HistoryDragProcessor historyDragProcessor = new HistoryDragProcessor(); + HistoryDragProcessor historyDragProcessor = new HistoryDragProcessor(history); final DragListener historyDragListener = newDragListener(historyDragProcessor, activity); final DragButton historyButton = getButton(views, R.id.cpp_button_history); if (historyButton != null) { diff --git a/app/src/main/java/org/solovyev/android/calculator/CalculatorBroadcaster.java b/app/src/main/java/org/solovyev/android/calculator/Broadcaster.java similarity index 88% rename from app/src/main/java/org/solovyev/android/calculator/CalculatorBroadcaster.java rename to app/src/main/java/org/solovyev/android/calculator/Broadcaster.java index 9219c974..1879c721 100644 --- a/app/src/main/java/org/solovyev/android/calculator/CalculatorBroadcaster.java +++ b/app/src/main/java/org/solovyev/android/calculator/Broadcaster.java @@ -1,5 +1,6 @@ package org.solovyev.android.calculator; +import android.app.Application; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -13,8 +14,10 @@ import java.util.Map; import javax.annotation.Nonnull; import javax.inject.Inject; +import javax.inject.Singleton; -public final class CalculatorBroadcaster implements SharedPreferences.OnSharedPreferenceChangeListener { +@Singleton +public class Broadcaster implements SharedPreferences.OnSharedPreferenceChangeListener { public static final String ACTION_INIT = "org.solovyev.android.calculator.INIT"; public static final String ACTION_EDITOR_STATE_CHANGED = "org.solovyev.android.calculator.EDITOR_STATE_CHANGED"; @@ -26,8 +29,8 @@ public final class CalculatorBroadcaster implements SharedPreferences.OnSharedPr private final Intents intents = new Intents(); @Inject - public CalculatorBroadcaster(@Nonnull Context context, @Nonnull SharedPreferences preferences, @Nonnull Bus bus, @Nonnull Handler handler) { - this.context = context; + public Broadcaster(@Nonnull Application application, @Nonnull SharedPreferences preferences, @Nonnull Bus bus, @Nonnull Handler handler) { + this.context = application; preferences.registerOnSharedPreferenceChangeListener(this); bus.register(this); handler.postDelayed(new Runnable() { diff --git a/app/src/main/java/org/solovyev/android/calculator/CalculatorApplication.java b/app/src/main/java/org/solovyev/android/calculator/CalculatorApplication.java index 7e8edfaa..73fde4a8 100644 --- a/app/src/main/java/org/solovyev/android/calculator/CalculatorApplication.java +++ b/app/src/main/java/org/solovyev/android/calculator/CalculatorApplication.java @@ -34,6 +34,7 @@ import org.acra.ACRA; import org.acra.ACRAConfiguration; import org.acra.sender.HttpSender; import org.solovyev.android.Android; +import org.solovyev.android.calculator.history.History; import org.solovyev.android.calculator.language.Language; import org.solovyev.android.calculator.language.Languages; import org.solovyev.android.calculator.model.AndroidCalculatorEngine; @@ -84,6 +85,12 @@ public class CalculatorApplication extends android.app.Application implements Sh @Named(AppModule.THREAD_UI) Executor uiThread; + @Inject + History history; + + @Inject + Broadcaster broadcaster; + @Override public void onCreate() { final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); diff --git a/app/src/main/java/org/solovyev/android/calculator/history/History.java b/app/src/main/java/org/solovyev/android/calculator/history/History.java index dd754822..7a4c39a9 100644 --- a/app/src/main/java/org/solovyev/android/calculator/history/History.java +++ b/app/src/main/java/org/solovyev/android/calculator/history/History.java @@ -57,14 +57,17 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Named; +import javax.inject.Singleton; import static android.text.TextUtils.isEmpty; +@Singleton public class History { public static final String TAG = App.subTag("History"); private static final ChangedEvent CHANGED_EVENT_RECENT = new ChangedEvent(true); private static final ChangedEvent CHANGED_EVENT_SAVED = new ChangedEvent(false); + private static final int MAX_INTERMEDIATE_STREAK = 5; @NonNull private final Runnable writeRecent = new WriteTask(true); @NonNull @@ -74,6 +77,8 @@ public class History { @Nonnull private final List saved = new ArrayList<>(); @Nonnull + private final Application application; + @Nonnull private final Bus bus; @Inject Handler handler; @@ -81,12 +86,10 @@ public class History { SharedPreferences preferences; @Inject Editor editor; - @Inject - Application application; - private boolean initialized; @Inject - public History(@NonNull Bus bus, @Nonnull @Named(AppModule.THREAD_INIT) Executor initThread) { + public History(@NonNull Application application, @NonNull Bus bus, @Nonnull @Named(AppModule.THREAD_INIT) Executor initThread) { + this.application = application; this.bus = bus; this.bus.register(this); initThread.execute(new Runnable() { @@ -117,7 +120,7 @@ public class History { } @Nonnull - private static List loadStates(@Nonnull File file) { + static List loadStates(@Nonnull File file) { if (!file.exists()) { return Collections.emptyList(); } @@ -219,7 +222,6 @@ public class History { Check.isTrue(saved.isEmpty()); recent.addAll(recentStates); saved.addAll(savedStates); - initialized = true; } }); } @@ -258,13 +260,17 @@ public class History { final List states = recent.asList(); final int statesCount = states.size(); + int streak = 0; for (int i = 1; i < statesCount; i++) { final HistoryState olderState = states.get(i - 1); final HistoryState newerState = states.get(i); final String olderText = olderState.editor.getTextString(); final String newerText = newerState.editor.getTextString(); - if (!isIntermediate(olderText, newerText, groupingSeparator)) { + if (streak >= MAX_INTERMEDIATE_STREAK || !isIntermediate(olderText, newerText, groupingSeparator)) { result.add(0, olderState); + streak = 0; + } else { + streak++; } } if (statesCount > 0) { @@ -330,9 +336,6 @@ public class History { @Subscribe public void onDisplayChanged(@Nonnull Display.ChangedEvent e) { - if (!initialized) { - return; - } final EditorState editorState = editor.getState(); final DisplayState displayState = e.newState; if (editorState.sequence != displayState.sequence) { diff --git a/app/src/main/java/org/solovyev/android/calculator/history/HistoryDragProcessor.java b/app/src/main/java/org/solovyev/android/calculator/history/HistoryDragProcessor.java index 602cf1f6..c33b7c7b 100644 --- a/app/src/main/java/org/solovyev/android/calculator/history/HistoryDragProcessor.java +++ b/app/src/main/java/org/solovyev/android/calculator/history/HistoryDragProcessor.java @@ -30,12 +30,15 @@ import org.solovyev.android.views.dragbutton.DragDirection; import org.solovyev.android.views.dragbutton.SimpleDragListener; import javax.annotation.Nonnull; -import javax.inject.Inject; public class HistoryDragProcessor implements SimpleDragListener.DragProcessor { - @Inject - History history; + @Nonnull + private final History history; + + public HistoryDragProcessor(@Nonnull History history) { + this.history = history; + } @Override public boolean processDragEvent(@Nonnull DragDirection direction, @Nonnull DragButton button, @Nonnull PointF startPoint, @Nonnull MotionEvent motionEvent) { diff --git a/app/src/main/java/org/solovyev/android/calculator/history/RecentHistory.java b/app/src/main/java/org/solovyev/android/calculator/history/RecentHistory.java index 0c78d8da..2e344a18 100644 --- a/app/src/main/java/org/solovyev/android/calculator/history/RecentHistory.java +++ b/app/src/main/java/org/solovyev/android/calculator/history/RecentHistory.java @@ -16,7 +16,7 @@ import java.util.List; public class RecentHistory { - private static final int MAX_HISTORY = 20; + private static final int MAX_HISTORY = 40; @NonNull private final List list = new LinkedList<>(); diff --git a/app/src/main/java/org/solovyev/android/calculator/widget/CalculatorWidget.java b/app/src/main/java/org/solovyev/android/calculator/widget/CalculatorWidget.java index e38e805c..c0928246 100644 --- a/app/src/main/java/org/solovyev/android/calculator/widget/CalculatorWidget.java +++ b/app/src/main/java/org/solovyev/android/calculator/widget/CalculatorWidget.java @@ -59,10 +59,10 @@ import javax.annotation.Nullable; import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT; import static android.content.Intent.ACTION_CONFIGURATION_CHANGED; import static android.os.Build.VERSION_CODES.JELLY_BEAN; -import static org.solovyev.android.calculator.CalculatorBroadcaster.ACTION_DISPLAY_STATE_CHANGED; -import static org.solovyev.android.calculator.CalculatorBroadcaster.ACTION_EDITOR_STATE_CHANGED; -import static org.solovyev.android.calculator.CalculatorBroadcaster.ACTION_INIT; -import static org.solovyev.android.calculator.CalculatorBroadcaster.ACTION_THEME_CHANGED; +import static org.solovyev.android.calculator.Broadcaster.ACTION_DISPLAY_STATE_CHANGED; +import static org.solovyev.android.calculator.Broadcaster.ACTION_EDITOR_STATE_CHANGED; +import static org.solovyev.android.calculator.Broadcaster.ACTION_INIT; +import static org.solovyev.android.calculator.Broadcaster.ACTION_THEME_CHANGED; import static org.solovyev.android.calculator.CalculatorReceiver.newButtonClickedIntent; public class CalculatorWidget extends AppWidgetProvider { diff --git a/app/src/test/java/org/solovyev/android/calculator/history/HistoryTest.java b/app/src/test/java/org/solovyev/android/calculator/history/HistoryTest.java index 98d57769..b3e301ab 100644 --- a/app/src/test/java/org/solovyev/android/calculator/history/HistoryTest.java +++ b/app/src/test/java/org/solovyev/android/calculator/history/HistoryTest.java @@ -35,10 +35,13 @@ import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.solovyev.android.CalculatorTestRunner; import org.solovyev.android.calculator.BuildConfig; +import org.solovyev.android.calculator.Display; import org.solovyev.android.calculator.DisplayState; import org.solovyev.android.calculator.Editor; import org.solovyev.android.calculator.EditorState; +import org.solovyev.android.calculator.jscl.JsclOperation; +import java.io.File; import java.util.List; import java.util.concurrent.Executor; @@ -47,6 +50,7 @@ import javax.annotation.Nonnull; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.mock; @@ -61,11 +65,10 @@ public class HistoryTest { @Before public void setUp() throws Exception { - history = new History(mock(Bus.class), mock(Executor.class)); + history = new History(RuntimeEnvironment.application, mock(Bus.class), mock(Executor.class)); history.handler = mock(Handler.class); history.preferences = mock(SharedPreferences.class); history.editor = mock(Editor.class); - history.application = RuntimeEnvironment.application; } @Test @@ -85,9 +88,11 @@ public class HistoryTest { addState("23547"); final List states = history.getRecent(); - assertEquals(2, states.size()); + assertEquals(3, states.size()); assertEquals("23547", states.get(0).editor.getTextString()); - assertEquals("123+3", states.get(1).editor.getTextString()); + // intermediate state + assertEquals("235", states.get(1).editor.getTextString()); + assertEquals("123+3", states.get(2).editor.getTextString()); } @Test @@ -257,4 +262,57 @@ public class HistoryTest { assertEquals(true, state.display.valid); assertNull(state.display.getResult()); } + + @Test + public void testShouldAddStateIfEditorAndDisplayAreInSync() throws Exception { + final EditorState editorState = EditorState.create("editor", 2); + when(history.editor.getState()).thenReturn(editorState); + + final DisplayState displayState = DisplayState.createError(JsclOperation.numeric, "test", editorState.sequence); + history.onDisplayChanged(new Display.ChangedEvent(DisplayState.empty(), displayState)); + + final List states = history.getRecent(); + assertEquals(1, states.size()); + assertSame(editorState, states.get(0).editor); + assertSame(displayState, states.get(0).display); + } + + @Test + public void testShouldNotAddStateIfEditorAndDisplayAreOutOfSync() throws Exception { + final EditorState editorState = EditorState.create("editor", 2); + when(history.editor.getState()).thenReturn(editorState); + + final DisplayState displayState = DisplayState.createError(JsclOperation.numeric, "test", editorState.sequence - 1); + history.onDisplayChanged(new Display.ChangedEvent(DisplayState.empty(), displayState)); + + final List states = history.getRecent(); + assertEquals(0, states.size()); + } + + @Test + public void testShouldLoadStates() throws Exception { + final List states = History.loadStates(new File(HistoryTest.class.getResource("recent-history.json").getFile())); + assertEquals(8, states.size()); + + HistoryState state = states.get(0); + assertEquals(1452770652381L, state.time); + assertEquals("", state.comment); + assertEquals("01 234 567 890 123 456 789", state.editor.getTextString()); + assertEquals(26, state.editor.selection); + assertEquals("1 234 567 890 123 460 000", state.display.text); + + state = states.get(4); + assertEquals(1452770626394L, state.time); + assertEquals("", state.comment); + assertEquals("985", state.editor.getTextString()); + assertEquals(3, state.editor.selection); + assertEquals("985", state.display.text); + + state = states.get(7); + assertEquals(1452770503823L, state.time); + assertEquals("", state.comment); + assertEquals("52", state.editor.getTextString()); + assertEquals(2, state.editor.selection); + assertEquals("52", state.display.text); + } } diff --git a/app/src/test/resources/org/solovyev/android/calculator/history/recent-history.json b/app/src/test/resources/org/solovyev/android/calculator/history/recent-history.json new file mode 100644 index 00000000..c3fc478d --- /dev/null +++ b/app/src/test/resources/org/solovyev/android/calculator/history/recent-history.json @@ -0,0 +1,82 @@ +[ + { + "e": { + "t": "01 234 567 890 123 456 789", + "s": 26 + }, + "d": { + "t": "1 234 567 890 123 460 000" + }, + "t": 1452770652381 + }, + { + "e": { + "t": "01 234 567 890 123 456", + "s": 22 + }, + "d": { + "t": "1 234 567 890 123 460" + }, + "t": 1452770651761 + }, + { + "e": { + "t": "01 234 567 890", + "s": 14 + }, + "d": { + "t": "1 234 567 890" + }, + "t": 1452770650247 + }, + { + "e": { + "t": "01 234", + "s": 6 + }, + "d": { + "t": "1 234" + }, + "t": 1452770632934 + }, + { + "e": { + "t": "985", + "s": 3 + }, + "d": { + "t": "985" + }, + "t": 1452770626394 + }, + { + "e": { + "t": "34 155+552", + "s": 10 + }, + "d": { + "t": "34 707" + }, + "t": 1452770497135 + }, + { + "e": { + "t": "9", + "s": 1 + }, + "d": { + "t": "9" + }, + "t": 1452770502906 + }, + { + "e": { + "t": "52", + "s": 2 + }, + "d": { + "t": "52" + }, + "t": 1452770503823 + } +] \ No newline at end of file