Further calculator state cleanups

Bug: 33265653

Preserve evaluation results in history earlier.

Make the mapping from saved state to evaluation state much more
explicit. Note that we no longer map EVALUATE to INIT. It seems
to make sense to do the animation the first time, even if a
rotation intervened.

Change-Id: I8dcf41d62305debb06f3fe3eece9818b21b67bcc
diff --git a/src/com/android/calculator2/Calculator.java b/src/com/android/calculator2/Calculator.java
index b877ed6..f39e6d3 100644
--- a/src/com/android/calculator2/Calculator.java
+++ b/src/com/android/calculator2/Calculator.java
@@ -100,7 +100,8 @@
                         // Not used for instant result evaluation.
         INIT,           // Very temporary state used as alternative to EVALUATE
                         // during reinitialization.  Do not animate on completion.
-        INIT_FOR_RESULT,  // Identical to INIT, but evaluation is known to terminate.
+        INIT_FOR_RESULT,  // Identical to INIT, but evaluation is known to terminate
+                          // with result, and current expression has been copied to history.
         ANIMATE,        // Result computed, animation to enlarge result window in progress.
         RESULT,         // Result displayed, formula invisible.
                         // If we are in RESULT state, the formula was evaluated without
@@ -116,9 +117,12 @@
     // initially evaluate assuming we were given a well-defined problem.  If we
     // were actually asked to compute sqrt(<extremely tiny negative number>) we produce 0
     // unless we are asked for enough precision that we can distinguish the argument from zero.
-    // ANIMATE, ERROR, and RESULT are translated to an INIT state if the application
+    // ERROR and RESULT are translated to INIT or INIT_FOR_RESULT state if the application
     // is restarted in that state.  This leads us to recompute and redisplay the result
-    // ASAP.
+    // ASAP. We avoid saving the ANIMATE state or activating history in that state.
+    // In INIT_FOR_RESULT, and RESULT state, a copy of the current
+    // expression has been saved in the history db; in the other non-ANIMATE states,
+    // it has not.
     // TODO: Possibly save a bit more information, e.g. its initial display string
     // or most significant digit position, to speed up restart.
 
@@ -301,6 +305,26 @@
     private HistoryFragment mHistoryFragment = new HistoryFragment();
 
     /**
+     * Map the old saved state to a new state reflecting requested result reevaluation.
+     */
+    private CalculatorState mapFromSaved(CalculatorState savedState) {
+        switch (savedState) {
+            case RESULT:
+            case INIT_FOR_RESULT:
+                // Evaluation is expected to terminate normally.
+                return CalculatorState.INIT_FOR_RESULT;
+            case ERROR:
+            case INIT:
+                return CalculatorState.INIT;
+            case EVALUATE:
+            case INPUT:
+                return savedState;
+            default:  // Includes ANIMATE state.
+                throw new AssertionError("Impossible saved state");
+        }
+    }
+
+    /**
      * Restore Evaluator state and mCurrentState from savedInstanceState.
      * Return true if the toolbar should be visible.
      */
@@ -346,9 +370,7 @@
             mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_EVALUATE, this);
         } else {
             // Just reevaluate.
-            setState((mCurrentState == CalculatorState.RESULT
-                    || mCurrentState == CalculatorState.INIT_FOR_RESULT) ?
-                    CalculatorState.INIT_FOR_RESULT : CalculatorState.INIT);
+            setState(mapFromSaved(mCurrentState));
             // Request evaluation when we know display width.
             mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_REQUIRE, this);
         }
@@ -540,6 +562,13 @@
         super.onDestroy();
     }
 
+    /**
+     * Destroy the evaluator and close the underlying database.
+     */
+    public void destroyEvaluator() {
+        mEvaluator.destroyEvaluator();
+    }
+
     @Override
     public void onActionModeStarted(ActionMode mode) {
         super.onActionModeStarted(mode);
@@ -1140,6 +1169,9 @@
         final int formulaTextColor = mFormulaText.getCurrentTextColor();
 
         if (animate) {
+            // Add current result to history.
+            mEvaluator.preserve(true);
+
             mResultText.announceForAccessibility(getResources().getString(R.string.desc_eq));
             mResultText.announceForAccessibility(mResultText.getText());
             setState(CalculatorState.ANIMATE);
@@ -1157,8 +1189,6 @@
             animatorSet.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation) {
-                    // Add current result to history.
-                    mEvaluator.preserve(true);
                     setState(CalculatorState.RESULT);
                     mCurrentAnimator = null;
                 }
diff --git a/src/com/android/calculator2/Evaluator.java b/src/com/android/calculator2/Evaluator.java
index ddfb0d6..64e1b6c 100644
--- a/src/com/android/calculator2/Evaluator.java
+++ b/src/com/android/calculator2/Evaluator.java
@@ -1864,4 +1864,14 @@
         sb.append(" Saved index = ").append(getSavedIndex()).append("\n");
         return sb.toString();
     }
+
+    /**
+     * Destroy the current evaluator, forcing getEvaluator to allocate a new one.
+     * This is needed for testing, since Robolectric apparently doesn't let us preserve
+     * an open databse across tests. Cf. https://github.com/robolectric/robolectric/issues/1890 .
+     */
+    public void destroyEvaluator() {
+        mExprDB.close();
+        evaluator = null;
+    }
 }