Add history and persistence support to Evaluator.

Bug: 31623549
Bug: 31686717

This version superficially works, but introduces a number of FIXME
comments that should be repaired before shipping. It may not stand
up well to monkey tests.

Add ExpressionDB.java that encapsulates a SQLite database to hold
the calculator history.

Change Evaluator to support simultaneous evaluation of many different
expressions, such as those in the history. In order to avoid UI thread
blocking, the evaluator now loads referenced subexpression on demand,
and multiple expressions can be evaluated simultaneously. We handle
the concurrency somewhat optimistically, arranging to have concurrent
evaluations not step on each other, at the expense of occasionally
throwing away a redundant result.

Change the expression serialization algorithm to make formulas more
compact. Do this now to minimize annoying database format changes.

Persist the saved clipboard and "memory" state.

Add two buttons in landscape mode to allow minimal access to "memory".

Since this involved a substantial rewrite of Evaluator.java, it
includes some drive-by cleanups and minor bug fixes.

Notably:
mMsdIndex was apparently always recomputed, but never actually
saved. Oops. This no doubt added a small amount of overhead.

We updated the decimal conversion size limits, hopefully addressing
b/30200325, possibly at some expense of slightly worse behavior
on old 32-bit devices.

Tests: Tested manually with a bit of added instrumentation.
Ran existing automated tests.

Change-Id: Ifae408d7b4b6cacd19f0e8f5aca146e9c653927e
diff --git a/src/com/android/calculator2/Calculator.java b/src/com/android/calculator2/Calculator.java
index b730425..8e4b240 100644
--- a/src/com/android/calculator2/Calculator.java
+++ b/src/com/android/calculator2/Calculator.java
@@ -77,8 +77,9 @@
 import java.io.ObjectOutput;
 import java.io.ObjectOutputStream;
 
-public class Calculator extends Activity implements OnTextSizeChangeListener, OnLongClickListener,
-        CalculatorFormula.OnPasteListener, AlertDialogFragment.OnClickListener {
+public class Calculator extends Activity
+        implements OnTextSizeChangeListener, OnLongClickListener, CalculatorFormula.OnPasteListener,
+        AlertDialogFragment.OnClickListener, Evaluator.EvaluationListener /* for main result */ {
 
     /**
      * Constant for an invalid resource id.
@@ -96,6 +97,7 @@
         RESULT,         // Result displayed, formula invisible.
                         // If we are in RESULT state, the formula was evaluated without
                         // error to initial precision.
+                        // The current formula is now also the last history entry.
         ERROR           // Error displayed: Formula visible, result shows error message.
                         // Display similar to INPUT state.
     }
@@ -209,6 +211,9 @@
 
     private HistoryFragment mHistoryFragment = new HistoryFragment();
 
+    // The user requested that the result currently being evaluated should be stored to "memory".
+    private boolean mStoreToMemoryRequested = false;
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -262,8 +267,8 @@
                 findViewById(R.id.op_sqr)
         };
 
-        mEvaluator = new Evaluator(this, mResultText);
-        mResultText.setEvaluator(mEvaluator);
+        mEvaluator = new Evaluator(this);
+        mResultText.setEvaluator(mEvaluator, Evaluator.MAIN_INDEX);
         KeyMaps.setActivity(this);
 
         mDragLayout = (DragLayout) findViewById(R.id.drag_layout);
@@ -294,12 +299,12 @@
                 } catch (Throwable ignored) {
                     // When in doubt, revert to clean state
                     mCurrentState = CalculatorState.INPUT;
-                    mEvaluator.clear();
+                    mEvaluator.clearMain();
                 }
             }
         } else {
             mCurrentState = CalculatorState.INPUT;
-            mEvaluator.clear();
+            mEvaluator.clearMain();
         }
 
         mFormulaText.setOnTextSizeChangeListener(this);
@@ -310,7 +315,7 @@
         onInverseToggled(savedInstanceState != null
                 && savedInstanceState.getBoolean(KEY_INVERSE_MODE));
 
-        onModeChanged(mEvaluator.getDegreeMode());
+        onModeChanged(mEvaluator.getDegreeMode(Evaluator.MAIN_INDEX));
         if (savedInstanceState != null &&
                 savedInstanceState.getBoolean(KEY_SHOW_TOOLBAR, true) == false) {
             mDisplayView.hideToolbar();
@@ -322,7 +327,7 @@
             // Just reevaluate.
             redisplayFormula();
             setState(CalculatorState.INIT);
-            mEvaluator.requireResult();
+            mEvaluator.requireResult(Evaluator.MAIN_INDEX, this, mResultText);
         } else {
             redisplayAfterFormulaChange();
         }
@@ -584,10 +589,10 @@
      */
     private void switchToInput(int button_id) {
         if (KeyMaps.isBinary(button_id) || KeyMaps.isSuffix(button_id)) {
-            mEvaluator.collapse();
+            mEvaluator.collapse(mEvaluator.getMaxIndex() /* Most recent history entry */);
         } else {
             announceClearedForAccessibility();
-            mEvaluator.clear();
+            mEvaluator.clearMain();
         }
         setState(CalculatorState.INPUT);
     }
@@ -612,7 +617,7 @@
      */
     private void addExplicitKeyToExpr(int id) {
         if (mCurrentState == CalculatorState.INPUT && id == R.id.op_sub) {
-            mEvaluator.getExpr().removeTrailingAdditiveOperators();
+            mEvaluator.getExpr(Evaluator.MAIN_INDEX).removeTrailingAdditiveOperators();
         }
         addKeyToExpr(id);
     }
@@ -621,15 +626,13 @@
         // TODO: Could do this more incrementally.
         redisplayFormula();
         setState(CalculatorState.INPUT);
+        mResultText.clear();
         if (haveUnprocessed()) {
-            mResultText.clear();
             // Force reevaluation when text is deleted, even if expression is unchanged.
             mEvaluator.touch();
         } else {
-            if (mEvaluator.getExpr().hasInterestingOps()) {
-                mEvaluator.evaluateAndShowResult();
-            } else {
-                mResultText.clear();
+            if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
+                mEvaluator.evaluateAndNotify(Evaluator.MAIN_INDEX, this, mResultText);
             }
         }
     }
@@ -689,9 +692,11 @@
                 break;
             case R.id.toggle_mode:
                 cancelIfEvaluating(false);
-                final boolean mode = !mEvaluator.getDegreeMode();
-                if (mCurrentState == CalculatorState.RESULT) {
-                    mEvaluator.collapse();  // Capture result evaluated in old mode
+                final boolean mode = !mEvaluator.getDegreeMode(Evaluator.MAIN_INDEX);
+                if (mCurrentState == CalculatorState.RESULT
+                        && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasTrigFuncs()) {
+                    // Capture current result evaluated in old mode.
+                    mEvaluator.collapse(mEvaluator.getMaxIndex());
                     redisplayFormula();
                 }
                 // In input mode, we reinterpret already entered trig functions.
@@ -701,10 +706,17 @@
                 showAndMaybeHideToolbar();
                 setState(CalculatorState.INPUT);
                 mResultText.clear();
-                if (!haveUnprocessed() && mEvaluator.getExpr().hasInterestingOps()) {
-                    mEvaluator.evaluateAndShowResult();
+                if (!haveUnprocessed()
+                        && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
+                    mEvaluator.evaluateAndNotify(mEvaluator.MAIN_INDEX, this, mResultText);
                 }
                 return;
+            case R.id.memory_store:
+                onMemoryStore();
+                return;
+            case R.id.memory_recall:
+                onMemoryRecall();
+                return;
             default:
                 cancelIfEvaluating(false);
                 if (haveUnprocessed()) {
@@ -721,7 +733,8 @@
     }
 
     void redisplayFormula() {
-        SpannableStringBuilder formula = mEvaluator.getExpr().toSpannableStringBuilder(this);
+        SpannableStringBuilder formula
+                = mEvaluator.getExpr(Evaluator.MAIN_INDEX).toSpannableStringBuilder(this);
         if (mUnprocessedChars != null) {
             // Add and highlight characters we couldn't process.
             formula.append(mUnprocessedChars, mUnprocessedColorSpan,
@@ -744,28 +757,36 @@
     }
 
     // Initial evaluation completed successfully.  Initiate display.
-    public void onEvaluate(int initDisplayPrec, int msd, int leastDigPos,
+    public void onEvaluate(long index, int initDisplayPrec, int msd, int leastDigPos,
             String truncatedWholeNumber) {
+        if (index != Evaluator.MAIN_INDEX) {
+            throw new AssertionError("Unexpected evaluation result index\n");
+        }
+        if (mStoreToMemoryRequested) {
+            mEvaluator.copyToMemory(Evaluator.MAIN_INDEX);
+            mStoreToMemoryRequested = false;
+        }
         // Invalidate any options that may depend on the current result.
         invalidateOptionsMenu();
 
-        mResultText.displayResult(initDisplayPrec, msd, leastDigPos, truncatedWholeNumber);
+        mResultText.onEvaluate(index, initDisplayPrec, msd, leastDigPos, truncatedWholeNumber);
         if (mCurrentState != CalculatorState.INPUT) { // in EVALUATE or INIT state
             onResult(mCurrentState != CalculatorState.INIT);
         }
     }
 
     // Reset state to reflect evaluator cancellation.  Invoked by evaluator.
-    public void onCancelled() {
-        // We should be in EVALUATE state.
+    public void onCancelled(long index) {
+        // Index is Evaluator.MAIN_INDEX. We should be in EVALUATE state.
         setState(CalculatorState.INPUT);
-        mResultText.clear();
+        mResultText.onCancelled(index);
     }
 
     // Reevaluation completed; ask result to redisplay current value.
-    public void onReevaluate()
+    public void onReevaluate(long index)
     {
-        mResultText.redisplay();
+        // Index is Evaluator.MAIN_INDEX.
+        mResultText.onReevaluate(index);
     }
 
     @Override
@@ -802,6 +823,7 @@
      */
     private boolean cancelIfEvaluating(boolean quiet) {
         if (mCurrentState == CalculatorState.EVALUATE) {
+            // TODO: Maybe just cancel main expression evaluation?
             mEvaluator.cancelAll(quiet);
             return true;
         } else {
@@ -818,10 +840,10 @@
         if (mCurrentState == CalculatorState.INPUT) {
             if (haveUnprocessed()) {
                 setState(CalculatorState.EVALUATE);
-                onError(R.string.error_syntax);
-            } else if (mEvaluator.getExpr().hasInterestingOps()) {
+                onError(Evaluator.MAIN_INDEX, R.string.error_syntax);
+            } else if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
                 setState(CalculatorState.EVALUATE);
-                mEvaluator.requireResult();
+                mEvaluator.requireResult(Evaluator.MAIN_INDEX, this, mResultText);
             }
         }
     }
@@ -840,13 +862,34 @@
         } else {
             mEvaluator.delete();
         }
-        if (mEvaluator.getExpr().isEmpty() && !haveUnprocessed()) {
+        if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).isEmpty() && !haveUnprocessed()) {
             // Resulting formula won't be announced, since it's empty.
             announceClearedForAccessibility();
         }
         redisplayAfterFormulaChange();
     }
 
+    private void onMemoryStore() {
+        if (mCurrentState == CalculatorState.RESULT) {
+            mEvaluator.copyToMemory(Evaluator.MAIN_INDEX);
+        } else {
+            // Defer the store until we have the actual result.
+            mStoreToMemoryRequested = true;
+            if (mCurrentState == CalculatorState.INPUT) {
+                onEquals();
+            }
+        }
+    }
+
+    private void onMemoryRecall() {
+        clearIfNotInputState();
+        long memoryIndex = mEvaluator.getMemoryIndex();
+        if (memoryIndex != 0) {
+            mEvaluator.appendExpr(mEvaluator.getMemoryIndex());
+            redisplayAfterFormulaChange();
+        }  // FIXME: Avoid the 0 case, e.g. by graying out button when memory is unavailable.
+    }
+
     private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
         final ViewGroupOverlay groupOverlay =
                 (ViewGroupOverlay) getWindow().getDecorView().getOverlay();
@@ -906,7 +949,7 @@
     }
 
     private void onClear() {
-        if (mEvaluator.getExpr().isEmpty() && !haveUnprocessed()) {
+        if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).isEmpty() && !haveUnprocessed()) {
             return;
         }
         cancelIfEvaluating(true);
@@ -916,7 +959,7 @@
             public void onAnimationEnd(Animator animation) {
                 mUnprocessedChars = null;
                 mResultText.clear();
-                mEvaluator.clear();
+                mEvaluator.clearMain();
                 setState(CalculatorState.INPUT);
                 showOrHideToolbar();
                 redisplayFormula();
@@ -925,7 +968,11 @@
     }
 
     // Evaluation encountered en error.  Display the error.
-    void onError(final int errorResourceId) {
+    @Override
+    public void onError(final long index, final int errorResourceId) {
+        if (index != Evaluator.MAIN_INDEX) {
+            throw new AssertionError("Unexpected error source");
+        }
         if (mCurrentState == CalculatorState.EVALUATE) {
             setState(CalculatorState.ANIMATE);
             mResultText.announceForAccessibility(getResources().getString(errorResourceId));
@@ -934,12 +981,12 @@
                         @Override
                         public void onAnimationEnd(Animator animation) {
                            setState(CalculatorState.ERROR);
-                           mResultText.displayError(errorResourceId);
+                           mResultText.onError(index, errorResourceId);
                         }
                     });
         } else if (mCurrentState == CalculatorState.INIT) {
             setState(CalculatorState.ERROR);
-            mResultText.displayError(errorResourceId);
+            mResultText.onError(index, errorResourceId);
         } else {
             mResultText.clear();
         }
@@ -1002,6 +1049,8 @@
             animatorSet.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation) {
+                    // Add current result to history.
+                    mEvaluator.preserve(true);
                     setState(CalculatorState.RESULT);
                     mCurrentAnimator = null;
                 }
@@ -1009,12 +1058,13 @@
 
             mCurrentAnimator = animatorSet;
             animatorSet.start();
-        } else /* No animation desired; get there fast, e.g. when restarting */ {
+        } else /* No animation desired; get there fast when restarting */ {
             mResultText.setScaleX(resultScale);
             mResultText.setScaleY(resultScale);
             mResultText.setTranslationY(resultTranslationY);
             mResultText.setTextColor(formulaTextColor);
             mFormulaContainer.setTranslationY(formulaTranslationY);
+            mEvaluator.represerve();
             setState(CalculatorState.RESULT);
         }
     }
@@ -1038,7 +1088,7 @@
     public void onClick(AlertDialogFragment fragment, int which) {
         if (which == DialogInterface.BUTTON_POSITIVE) {
             // Timeout extension request.
-            mEvaluator.setLongTimeOut();
+            mEvaluator.setLongTimeout();
         }
     }
 
@@ -1059,7 +1109,7 @@
 
         // Show the fraction option when displaying a rational result.
         menu.findItem(R.id.menu_fraction).setVisible(mCurrentState == CalculatorState.RESULT
-                && mEvaluator.getResult().exactlyDisplayable());
+                && mEvaluator.getResult(Evaluator.MAIN_INDEX).exactlyDisplayable());
 
         return true;
     }
@@ -1098,7 +1148,7 @@
     }
 
     private void displayFraction() {
-        UnifiedReal result = mEvaluator.getResult();
+        UnifiedReal result = mEvaluator.getResult(Evaluator.MAIN_INDEX);
         displayMessage(getString(R.string.menu_fraction),
                 KeyMaps.translateResult(result.toNiceString()));
     }
@@ -1156,7 +1206,7 @@
                 } else {
                     boolean isDigit = KeyMaps.digVal(k) != KeyMaps.NOT_DIGIT;
                     if (current == 0 && (isDigit || k == R.id.dec_point)
-                            && mEvaluator.getExpr().hasTrailingConstant()) {
+                            && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasTrailingConstant()) {
                         // Refuse to concatenate pasted content to trailing constant.
                         // This makes pasting of calculator results more consistent, whether or
                         // not the old calculator instance is still around.
@@ -1205,6 +1255,14 @@
         showOrHideToolbar();
     }
 
+    private void clearIfNotInputState() {
+        if (mCurrentState == CalculatorState.ERROR
+                || mCurrentState == CalculatorState.RESULT) {
+            setState(CalculatorState.INPUT);
+            mEvaluator.clearMain();
+        }
+    }
+
     @Override
     public boolean onPaste(ClipData clip) {
         final ClipData.Item item = clip.getItemCount() == 0 ? null : clip.getItemAt(0);
@@ -1216,12 +1274,8 @@
         // Check if the item is a previously copied result, otherwise paste as raw text.
         final Uri uri = item.getUri();
         if (uri != null && mEvaluator.isLastSaved(uri)) {
-            if (mCurrentState == CalculatorState.ERROR
-                    || mCurrentState == CalculatorState.RESULT) {
-                setState(CalculatorState.INPUT);
-                mEvaluator.clear();
-            }
-            mEvaluator.appendSaved();
+            clearIfNotInputState();
+            mEvaluator.appendExpr(mEvaluator.getSavedIndex());
             redisplayAfterFormulaChange();
         } else {
             addChars(item.coerceToText(this).toString(), false);
diff --git a/src/com/android/calculator2/CalculatorExpr.java b/src/com/android/calculator2/CalculatorExpr.java
index 41dfe13..f20d2bf 100644
--- a/src/com/android/calculator2/CalculatorExpr.java
+++ b/src/com/android/calculator2/CalculatorExpr.java
@@ -22,7 +22,6 @@
 import android.text.Spanned;
 import android.text.style.TtsSpan;
 import android.text.style.TtsSpan.TextBuilder;
-import android.util.Log;
 
 import java.math.BigInteger;
 import java.io.DataInput;
@@ -47,6 +46,35 @@
  * when reading it back in.
  */
 class CalculatorExpr {
+    /**
+     * An interface for resolving expression indices in embedded subexpressions to
+     * the associated CalculatorExpr, and associating a UnifiedReal result with it.
+     * All methods are thread-safe in the strong sense; they may be called asynchronously
+     * at any time from any thread.
+     */
+    public interface ExprResolver {
+        /*
+         * Retrieve the expression corresponding to index.
+         */
+        CalculatorExpr getExpr(long index);
+        /*
+         * Retrive the degree mode associated with the expression at index i.
+         */
+        boolean getDegreeMode(long index);
+        /*
+         * Retrieve the stored result for the expression at index, or return null.
+         */
+        UnifiedReal getResult(long index);
+        /*
+         * Atomically test for an existing result, and set it if there was none.
+         * Return the prior result if there was one, or the new one if there was not.
+         * May only be called after getExpr.
+         */
+        UnifiedReal putResultIfAbsent(long index, UnifiedReal result);
+        // FIXME: Check that long timeouts for embedded expressions are propagated
+        // correctly.
+    }
+
     private ArrayList<Token> mExpr;  // The actual representation
                                      // as a list of tokens.  Constant
                                      // tokens are always nonempty.
@@ -60,7 +88,9 @@
         abstract TokenKind kind();
 
         /**
-         * Write kind as Byte followed by data needed by subclass constructor.
+         * Write token as either a very small Byte containing the TokenKind,
+         * followed by data needed by subclass constructor,
+         * or as a byte >= 0x20 directly describing the OPERATOR token.
          */
         abstract void write(DataOutput out) throws IOException;
 
@@ -77,17 +107,17 @@
      * Representation of an operator token
      */
     private static class Operator extends Token {
+        // TODO: rename id.
         public final int id; // We use the button resource id
         Operator(int resId) {
             id = resId;
         }
-        Operator(DataInput in) throws IOException {
-            id = in.readInt();
+        Operator(byte op) throws IOException {
+            id = KeyMaps.fromByte(op);
         }
         @Override
         void write(DataOutput out) throws IOException {
-            out.writeByte(TokenKind.OPERATOR.ordinal());
-            out.writeInt(id);
+            out.writeByte(KeyMaps.toByte(id));
         }
         @Override
         public CharSequence toCharSequence(Context context) {
@@ -114,28 +144,44 @@
         private String mWhole;  // String preceding decimal point.
         private String mFraction; // String after decimal point.
         private int mExponent;  // Explicit exponent, only generated through addExponent.
+        private static int SAW_DECIMAL = 0x1;
+        private static int HAS_EXPONENT = 0x2;
 
         Constant() {
             mWhole = "";
             mFraction = "";
-            mSawDecimal = false;
-            mExponent = 0;
+            // mSawDecimal = false;
+            // mExponent = 0;
         };
 
         Constant(DataInput in) throws IOException {
             mWhole = in.readUTF();
-            mSawDecimal = in.readBoolean();
-            mFraction = in.readUTF();
-            mExponent = in.readInt();
+            byte flags = in.readByte();
+            if ((flags & SAW_DECIMAL) != 0) {
+                mSawDecimal = true;
+                mFraction = in.readUTF();
+            } else {
+                // mSawDecimal = false;
+                mFraction = "";
+            }
+            if ((flags & HAS_EXPONENT) != 0) {
+                mExponent = in.readInt();
+            }
         }
 
         @Override
         void write(DataOutput out) throws IOException {
+            byte flags = (byte)((mSawDecimal ? SAW_DECIMAL : 0)
+                    | (mExponent != 0 ? HAS_EXPONENT : 0));
             out.writeByte(TokenKind.CONSTANT.ordinal());
             out.writeUTF(mWhole);
-            out.writeBoolean(mSawDecimal);
-            out.writeUTF(mFraction);
-            out.writeInt(mExponent);
+            out.writeByte(flags);
+            if (mSawDecimal) {
+                out.writeUTF(mFraction);
+            }
+            if (mExponent != 0) {
+                out.writeInt(mExponent);
+            }
         }
 
         // Given a button press, append corresponding digit.
@@ -266,105 +312,43 @@
         }
     }
 
-    // Hash maps used to detect duplicate subexpressions when we write out CalculatorExprs and
-    // read them back in.
-    private static final ThreadLocal<IdentityHashMap<UnifiedReal, Integer>>outMap =
-            new ThreadLocal<IdentityHashMap<UnifiedReal, Integer>>();
-        // Maps expressions to indices on output
-    private static final ThreadLocal<HashMap<Integer, PreEval>>inMap =
-            new ThreadLocal<HashMap<Integer, PreEval>>();
-        // Maps expressions to indices on output
-    private static final ThreadLocal<Integer> exprIndex = new ThreadLocal<Integer>();
-
-    /**
-     * Prepare for expression output.
-     * Initializes map that will lbe used to avoid duplicating shared subexpressions.
-     * This avoids a potential exponential blow-up in the expression size.
-     */
-    public static void initExprOutput() {
-        outMap.set(new IdentityHashMap<UnifiedReal, Integer>());
-        exprIndex.set(Integer.valueOf(0));
-    }
-
-    /**
-     * Prepare for expression input.
-     * Initializes map that will be used to reconstruct shared subexpressions.
-     */
-    public static void initExprInput() {
-        inMap.set(new HashMap<Integer, PreEval>());
-    }
-
     /**
      * The "token" class for previously evaluated subexpressions.
      * We treat previously evaluated subexpressions as tokens.  These are inserted when we either
      * continue an expression after evaluating some of it, or copy an expression and paste it back
      * in.
+     * This only contains enough information to allow us to display the expression in a
+     * formula, or reevaluate the expression with the aid of an ExprResolver; we no longer
+     * cache the result. The expression corresponding to the index can be obtained through
+     * the ExprResolver, which looks it up in a subexpression database.
      * The representation includes a UnifiedReal value.  In order to
      * support saving and restoring, we also include the underlying expression itself, and the
      * context (currently just degree mode) used to evaluate it.  The short string representation
      * is also stored in order to avoid potentially expensive recomputation in the UI thread.
      */
     private static class PreEval extends Token {
-        public final UnifiedReal value;
-        private final CalculatorExpr mExpr;
-        private final EvalContext mContext;
+        public final long mIndex;
         private final String mShortRep;  // Not internationalized.
-        PreEval(UnifiedReal val, CalculatorExpr expr, EvalContext ec, String shortRep) {
-            value = val;
-            mExpr = expr;
-            mContext = ec;
+        PreEval(long index, String shortRep) {
+            mIndex = index;
             mShortRep = shortRep;
         }
-        // In writing out PreEvals, we are careful to avoid writing out duplicates.  We conclude
-        // that two expressions are duplicates if they have the same UnifiedReal value.  This
-        // avoids a potential exponential blow up in certain off cases and redundant evaluation
-        // after reading them back in.  The parameter hash map maps expressions we've seen
-        // before to their index.
         @Override
+        // This writes out only a shallow representation of the result, without
+        // information about subexpressions. To write out a deep representation, we
+        // find referenced subexpressions, and iteratively write those as well.
         public void write(DataOutput out) throws IOException {
             out.writeByte(TokenKind.PRE_EVAL.ordinal());
-            Integer index = outMap.get().get(value);
-            if (index == null) {
-                int nextIndex = exprIndex.get() + 1;
-                exprIndex.set(nextIndex);
-                outMap.get().put(value, nextIndex);
-                out.writeInt(nextIndex);
-                mExpr.write(out);
-                mContext.write(out);
-                out.writeUTF(mShortRep);
-            } else {
-                // Just write out the index
-                out.writeInt(index);
+            if (mIndex > Integer.MAX_VALUE || mIndex < Integer.MIN_VALUE) {
+                // This would be millions of expressions per day for the life of the device.
+                throw new AssertionError("Expression index too big");
             }
+            out.writeInt((int)mIndex);
+            out.writeUTF(mShortRep);
         }
         PreEval(DataInput in) throws IOException {
-            int index = in.readInt();
-            PreEval prev = inMap.get().get(index);
-            if (prev == null) {
-                mExpr = new CalculatorExpr(in);
-                mContext = new EvalContext(in, mExpr.mExpr.size());
-                // Recompute other fields We currently do this in the UI thread, but we only
-                // create PreEval expressions that were previously successfully evaluated, and
-                // thus don't diverge.  We also only evaluate to a constructive real, which
-                // involves substantial work only in fairly contrived circumstances.
-                // TODO: Deal better with slow evaluations.
-                EvalRet res = null;
-                try {
-                    res = mExpr.evalExpr(0, mContext);
-                } catch (SyntaxException e) {
-                    // Should be impossible, since we only write out
-                    // expressions that can be evaluated.
-                    Log.e("Calculator", "Unexpected syntax exception" + e);
-                }
-                value = res.val;
-                mShortRep = in.readUTF();
-                inMap.get().put(index, this);
-            } else {
-                value = prev.value;
-                mExpr = prev.mExpr;
-                mContext = prev.mContext;
-                mShortRep = prev.mShortRep;
-            }
+            mIndex = in.readInt();
+            mShortRep = in.readUTF();
         }
         @Override
         public CharSequence toCharSequence(Context context) {
@@ -383,15 +367,18 @@
      * Read token from in.
      */
     public static Token newToken(DataInput in) throws IOException {
-        TokenKind kind = tokenKindValues[in.readByte()];
-        switch(kind) {
-        case CONSTANT:
-            return new Constant(in);
-        case OPERATOR:
-            return new Operator(in);
-        case PRE_EVAL:
-            return new PreEval(in);
-        default: throw new IOException("Bad save file format");
+        byte kindByte = in.readByte();
+        if (kindByte < 0x20) {
+            TokenKind kind = tokenKindValues[kindByte];
+            switch(kind) {
+            case CONSTANT:
+                return new Constant(in);
+            case PRE_EVAL:
+                return new PreEval(in);
+            default: throw new IOException("Bad save file format");
+            }
+        } else {
+            return new Operator(kindByte);
         }
     }
 
@@ -441,7 +428,7 @@
     /**
      * Does this expression end with a binary operator?
      */
-    private boolean hasTrailingBinary() {
+    boolean hasTrailingBinary() {
         int s = mExpr.size();
         if (s == 0) return false;
         Token t = mExpr.get(s-1);
@@ -612,14 +599,13 @@
     /**
      * Return a new expression consisting of a single token representing the current pre-evaluated
      * expression.
-     * The caller supplies the value, degree mode, and short string representation, which must
-     * have been previously computed.  Thus this is guaranteed to terminate reasonably quickly.
+     * The caller supplies the expression index and short string representation.
+     * The expression must have been previously evaluated.
      */
-    public CalculatorExpr abbreviate(UnifiedReal val, boolean dm, String sr) {
+    public CalculatorExpr abbreviate(long index, String sr) {
         CalculatorExpr result = new CalculatorExpr();
         @SuppressWarnings("unchecked")
-        Token t = new PreEval(val, new CalculatorExpr((ArrayList<Token>) mExpr.clone()),
-                new EvalContext(dm, mExpr.size()), sr);
+        Token t = new PreEval(index, sr);
         result.mExpr.add(t);
         return result;
     }
@@ -644,14 +630,17 @@
     private static class EvalContext {
         public final int mPrefixLength; // Length of prefix to evaluate. Not explicitly saved.
         public final boolean mDegreeMode;
+        public final ExprResolver mExprResolver;  // Reconstructed, not saved.
         // If we add any other kinds of evaluation modes, they go here.
-        EvalContext(boolean degreeMode, int len) {
+        EvalContext(boolean degreeMode, int len, ExprResolver er) {
             mDegreeMode = degreeMode;
             mPrefixLength = len;
+            mExprResolver = er;
         }
-        EvalContext(DataInput in, int len) throws IOException {
+        EvalContext(DataInput in, int len, ExprResolver er) throws IOException {
             mDegreeMode = in.readBoolean();
             mPrefixLength = len;
+            mExprResolver = er;
         }
         void write(DataOutput out) throws IOException {
             out.writeBoolean(mDegreeMode);
@@ -714,8 +703,16 @@
             return new EvalRet(i+1,new UnifiedReal(c.toRational()));
         }
         if (t instanceof PreEval) {
-            final PreEval p = (PreEval)t;
-            return new EvalRet(i+1, p.value);
+            final long index = ((PreEval)t).mIndex;
+            UnifiedReal res = ec.mExprResolver.getResult(index);
+            if (res == null) {
+                CalculatorExpr nestedExpr = ec.mExprResolver.getExpr(index);
+                EvalContext newEc = new EvalContext(ec.mExprResolver.getDegreeMode(index),
+                        nestedExpr.trailingBinaryOpsStart(), ec.mExprResolver);
+                EvalRet new_res = nestedExpr.evalExpr(0, newEc);
+                res = ec.mExprResolver.putResultIfAbsent(index, new_res.val);
+            }
+            return new EvalRet(i+1, res);
         }
         EvalRet argVal;
         switch(((Operator)(t)).id) {
@@ -1007,7 +1004,7 @@
      *
      * @param degreeMode use degrees rather than radians
      */
-    UnifiedReal eval(boolean degreeMode) throws SyntaxException
+    UnifiedReal eval(boolean degreeMode, ExprResolver er) throws SyntaxException
                         // And unchecked exceptions thrown by UnifiedReal, CR,
                         // and BoundedRational.
     {
@@ -1017,7 +1014,7 @@
             // expressions, and don't generate an error where we previously displayed an instant
             // result.  This reflects the Android L design.
             int prefixLen = trailingBinaryOpsStart();
-            EvalContext ec = new EvalContext(degreeMode, prefixLen);
+            EvalContext ec = new EvalContext(degreeMode, prefixLen, er);
             EvalRet res = evalExpr(0, ec);
             if (res.pos != prefixLen) {
                 throw new SyntaxException("Failed to parse full expression");
diff --git a/src/com/android/calculator2/CalculatorResult.java b/src/com/android/calculator2/CalculatorResult.java
index e14c8d2..caf72fe 100644
--- a/src/com/android/calculator2/CalculatorResult.java
+++ b/src/com/android/calculator2/CalculatorResult.java
@@ -47,12 +47,14 @@
 
 // A text widget that is "infinitely" scrollable to the right,
 // and obtains the text to display via a callback to Logic.
-public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenuItemClickListener {
+public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenuItemClickListener,
+        Evaluator.EvaluationListener, Evaluator.CharMetricsInfo {
     static final int MAX_RIGHT_SCROLL = 10000000;
     static final int INVALID = MAX_RIGHT_SCROLL + 10000;
         // A larger value is unlikely to avoid running out of space
     final OverScroller mScroller;
     final GestureDetector mGestureDetector;
+    private long mIndex;  // Index of expression we are displaying.
     private Evaluator mEvaluator;
     private boolean mScrollable = false;
                             // A scrollable result is currently displayed.
@@ -213,8 +215,9 @@
         setContentDescription(context.getString(R.string.desc_result));
     }
 
-    void setEvaluator(Evaluator evaluator) {
+    void setEvaluator(Evaluator evaluator, long index) {
         mEvaluator = evaluator;
+        mIndex = index;
     }
 
     // Compute maximum digit width the hard way.
@@ -300,12 +303,8 @@
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
     }
 
-    /**
-     * Return the number of additional digit widths required to add digit separators to
-     * the supplied string prefix.
-     * The string prefix is assumed to represent a whole number, after skipping leading non-digits.
-     * Callable from non-UI thread.
-     */
+    // From Evaluator.CharMetricsInfo.
+    @Override
     public float separatorChars(String s, int len) {
         int start = 0;
         while (start < len && !Character.isDigit(s.charAt(start))) {
@@ -322,20 +321,16 @@
         }
     }
 
-    /**
-     * Return extra width credit for absence of ellipsis, as fraction of a digit width.
-     * May be called by non-UI thread.
-     */
+    // From Evaluator.CharMetricsInfo.
+    @Override
     public float getNoEllipsisCredit() {
         synchronized(mWidthLock) {
             return mNoEllipsisCredit;
         }
     }
 
-    /**
-     * Return extra width credit for presence of a decimal point, as fraction of a digit width.
-     * May be called by non-UI thread.
-     */
+    // From Evaluator.CharMetricsInfo.
+    @Override
     public float getDecimalCredit() {
         synchronized(mWidthLock) {
             return mDecimalCredit;
@@ -355,6 +350,8 @@
      * Initiate display of a new result.
      * Only called from UI thread.
      * The parameters specify various properties of the result.
+     * @param index Index of expression that was just evaluated. Currently ignored, since we only
+     *            expect notification for the expression result being displayed.
      * @param initPrec Initial display precision computed by evaluator. (1 = tenths digit)
      * @param msd Position of most significant digit.  Offset from left of string.
                   Evaluator.INVALID_MSD if unknown.
@@ -363,7 +360,9 @@
      * @param truncatedWholePart Result up to but not including decimal point.
                                  Currently we only use the length.
      */
-    void displayResult(int initPrec, int msd, int leastDigPos, String truncatedWholePart) {
+    @Override
+    public void onEvaluate(long index, int initPrec, int msd, int leastDigPos,
+            String truncatedWholePart) {
         initPositions(initPrec, msd, leastDigPos, truncatedWholePart);
         redisplay();
     }
@@ -492,7 +491,8 @@
      * Display error message indicated by resourceId.
      * UI thread only.
      */
-    void displayError(int resourceId) {
+    @Override
+    public void onError(long index, int resourceId) {
         mValid = true;
         setLongClickable(false);
         mScrollable = false;
@@ -712,8 +712,8 @@
         final boolean truncated[] = new boolean[1];
         final boolean negative[] = new boolean[1];
         final int requestedPrecOffset[] = {precOffset};
-        final String rawResult = mEvaluator.getString(requestedPrecOffset, mMaxCharOffset,
-                maxSize, truncated, negative);
+        final String rawResult = mEvaluator.getString(mIndex, requestedPrecOffset, mMaxCharOffset,
+                maxSize, truncated, negative, this);
         return formatResult(rawResult, requestedPrecOffset[0], maxSize, truncated[0], negative[0],
                 lastDisplayedOffset, forcePrecision, forceSciNotation, insertCommas);
    }
@@ -753,7 +753,7 @@
         }
         // It's reasonable to compute and copy the exact result instead.
         final int nonNegLsdOffset = Math.max(0, mLsdOffset);
-        final String rawResult = mEvaluator.getResult().toStringTruncated(nonNegLsdOffset);
+        final String rawResult = mEvaluator.getResult(mIndex).toStringTruncated(nonNegLsdOffset);
         final String formattedResult = formatResult(rawResult, nonNegLsdOffset, MAX_COPY_SIZE,
                 false, rawResult.charAt(0) == '-', null, true /* forcePrecision */,
                 false /* forceSciNotation */, false /* insertCommas */);
@@ -762,9 +762,10 @@
 
     /**
      * Return the maximum number of characters that will fit in the result display.
-     * May be called asynchronously from non-UI thread.
+     * May be called asynchronously from non-UI thread. From Evaluator.CharMetricsInfo.
      */
-    int getMaxChars() {
+    @Override
+    public int getMaxChars() {
         int result;
         synchronized(mWidthLock) {
             result = (int) Math.floor(mWidthConstraint / mCharWidth);
@@ -801,11 +802,21 @@
         setLongClickable(false);
     }
 
+    @Override
+    public void onCancelled(long index) {
+        clear();
+    }
+
     /**
      * Refresh display.
-     * Only called in UI thread.
+     * Only called in UI thread. Index argument is currently ignored.
      */
-    void redisplay() {
+    @Override
+    public void onReevaluate(long index) {
+        redisplay();
+    }
+
+    public void redisplay() {
         if (mScroller.isFinished() && length() > 0) {
             setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
         }
@@ -1014,7 +1025,7 @@
                 (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
         // We include a tag URI, to allow us to recognize our own results and handle them
         // specially.
-        ClipData.Item newItem = new ClipData.Item(text, null, mEvaluator.capture());
+        ClipData.Item newItem = new ClipData.Item(text, null, mEvaluator.capture(mIndex));
         String[] mimeTypes = new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN};
         ClipData cd = new ClipData("calculator result", mimeTypes, newItem);
         clipboard.setPrimaryClip(cd);
@@ -1025,7 +1036,7 @@
     public boolean onMenuItemClick(MenuItem item) {
         switch (item.getItemId()) {
             case R.id.menu_copy:
-                if (mEvaluator.reevaluationInProgress()) {
+                if (mEvaluator.evaluationInProgress(mIndex)) {
                     // Refuse to copy placeholder characters.
                     return false;
                 } else {
diff --git a/src/com/android/calculator2/Evaluator.java b/src/com/android/calculator2/Evaluator.java
index fe2862c..184e733 100644
--- a/src/com/android/calculator2/Evaluator.java
+++ b/src/com/android/calculator2/Evaluator.java
@@ -16,6 +16,7 @@
 
 package com.android.calculator2;
 
+import android.app.Activity;
 import android.app.AlertDialog;
 import android.content.DialogInterface;
 import android.content.SharedPreferences;
@@ -28,38 +29,47 @@
 
 import com.hp.creals.CR;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.DataInput;
+import java.io.DataInputStream;
 import java.io.DataOutput;
+import java.io.DataOutputStream;
 import java.io.IOException;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.Date;
 import java.util.Random;
 import java.util.TimeZone;
 
 /**
- * This implements the calculator evaluation logic.  The underlying expression is constructed and
- * edited with append(), delete(), and clear().  An evaluation an then be started with a call to
- * evaluateAndShowResult() or requireResult().  This starts an asynchronous computation, which
- * requests display of the initial result, when available.  When initial evaluation is complete,
- * it calls the calculator onEvaluate() method.  This occurs in a separate event, possibly quite a
- * bit later.  Once a result has been computed, and before the underlying expression is modified,
- * the getString() method may be used to produce Strings that represent approximations to various
+ * This implements the calculator evaluation logic.
+ * Logically this maintains a signed integer indexed set of expressions, one of which
+ * is distinguished as the main expression.
+ * The main expression is constructed and edited with append(), delete(), etc.
+ * An evaluation an then be started with a call to evaluateAndNotify() or requireResult().
+ * This starts an asynchronous computation, which requests display of the initial result, when
+ * available.  When initial evaluation is complete, it calls the associated listener's
+ * onEvaluate() method.  This occurs in a separate event, possibly quite a bit later.  Once a
+ * result has been computed, and before the underlying expression is modified, the
+ * getString(index) method may be used to produce Strings that represent approximations to various
  * precisions.
  *
  * Actual expressions being evaluated are represented as {@link CalculatorExpr}s.
  *
- * The Evaluator owns the expression being edited and all associated state needed for evaluating
- * it.  It provides functionality for saving and restoring this state.  However the current
- * CalculatorExpr is exposed to the client, and may be directly accessed after cancelling any
+ * The Evaluator holds the expressions and all associated state needed for evaluating
+ * them.  It provides functionality for saving and restoring this state.  However the underlying
+ * CalculatorExprs are exposed to the client, and may be directly accessed after cancelling any
  * in-progress computations by invoking the cancelAll() method.
  *
  * When evaluation is requested, we invoke the eval() method on the CalculatorExpr from a
- * background AsyncTask.  A subsequent getString() callback returns immediately, though it may
- * return a result containing placeholder ' ' characters.  If we had to return palceholder
- * characters, we start a background task, which invokes the onReevaluate() callback when it
- * completes.  In either case, the background task computes the appropriate result digits by
- * evaluating the UnifiedReal returned by CalculatorExpr.eval() to the required
+ * background AsyncTask.  A subsequent getString() call for the same expression index returns
+ * immediately, though it may return a result containing placeholder ' ' characters.  If we had to
+ * return palceholder characters, we start a background task, which invokes the onReevaluate()
+ * callback when it completes.  In either case, the background task computes the appropriate
+ * result digits by evaluating the UnifiedReal returned by CalculatorExpr.eval() to the required
  * precision.
  *
  * We cache the best decimal approximation we have already computed.  We compute generously to
@@ -86,15 +96,115 @@
  * We ensure that only one evaluation of either kind (AsyncEvaluator or AsyncReevaluator) is
  * running at a time.
  */
-public class Evaluator {
+public class Evaluator implements CalculatorExpr.ExprResolver {
+
+    public interface EvaluationListener {
+        /**
+         * Called if evaluation was explicitly cancelled or evaluation timed out.
+         */
+        public void onCancelled(long index);
+        /**
+         * Called if evaluation resulted in an error.
+         */
+        public void onError(long index, int errorId);
+        /**
+         * Called if evaluation completed normally.
+         * @param index index of expression whose evaluation completed
+         * @param initPrecOffset the offset used for initial evaluation
+         * @param msdIndex index of first non-zero digit in the computed result string
+         * @param lsdOffset offset of last digit in result if result has finite decimal
+         *        expansion
+         * @param truncatedWholePart the integer part of the result
+         */
+        public void onEvaluate(long index, int initPrecOffset, int msdIndex, int lsdOffset,
+                String truncatedWholePart);
+        /**
+         * Called in response to a reevaluation request, once more precision is available.
+         * Typically the listener wil respond by calling getString() to retrieve the new
+         * better approximation.
+         */
+        public void onReevaluate(long index);  // More precision is now available; please redraw.
+    }
+
+    /**
+     * A query interface for derived information based on character widths.
+     * This provides information we need to calculate the "preferred precision offset" used
+     * to display the initial result. It's used to compute the number of digits we can actually
+     * display. All methods are callable from any thread.
+     */
+    public interface CharMetricsInfo {
+        /**
+         * Return the maximum number of (adjusted, digit-width) characters that will fit in the
+         * result display.  May be called asynchronously from non-UI thread.
+         */
+       public int getMaxChars();
+        /**
+         * Return the number of additional digit widths required to add digit separators to
+         * the supplied string prefix.
+         * The prefix consists of the first len characters of string s, which is presumed to
+         * represent a whole number. Callable from non-UI thread.
+         */
+        public float separatorChars(String s, int len);
+        /**
+         * Return extra width credit for presence of a decimal point, as fraction of a digit width.
+         * May be called by non-UI thread.
+         */
+        public float getDecimalCredit();
+        /**
+         * Return extra width credit for absence of ellipsis, as fraction of a digit width.
+         * May be called by non-UI thread.
+         */
+        public float getNoEllipsisCredit();
+    }
+
+    /**
+     * A CharMetricsInfo that can be used when we are really only interested in computing
+     * short representations to be embedded on formulas.
+     */
+    private class DummyCharMetricsInfo implements CharMetricsInfo {
+        @Override
+        public int getMaxChars() {
+            return SHORT_TARGET_LENGTH + 10;
+        }
+        @Override
+        public float separatorChars(String s, int len) {
+            return 0;
+        }
+        @Override
+        public float getDecimalCredit() {
+            return 0;
+        }
+        @Override
+        public float getNoEllipsisCredit() {
+            return 0;
+        }
+    }
+
+    private final DummyCharMetricsInfo mDummyCharMetricsInfo = new DummyCharMetricsInfo();
+
+    public static final long MAIN_INDEX = 0;  // Index of main expression.
+    // Once final evaluation of an expression is complete, or when we need to save
+    // a partial result, we copy the expression to a non-zero index.
+    // At that point, the expression no longer changes, and is preserved
+    // until the entire history is cleared. Only expressions at nonzero indices
+    // may be embedded in other expressions.
+    // To update e.g. "memory" contents, we copy the corresponding expression to a permanent
+    // index, and then remember that index.
+    private long mSavedIndex;  // Index of "saved" expression mirroring clipboard. 0 if unused.
+    private long mMemoryIndex;  // Index of "memory" expression. 0 if unused.
 
     // When naming variables and fields, "Offset" denotes a character offset in a string
     // representing a decimal number, where the offset is relative to the decimal point.  1 =
     // tenths position, -1 = units position.  Integer.MAX_VALUE is sometimes used for the offset
     // of the last digit in an a nonterminating decimal expansion.  We use the suffix "Index" to
-    // denote a zero-based absolute index into such a string.
+    // denote a zero-based absolute index into such a string. (In other contexts, like above,
+    // we also use "index" to refer to the key in mExprs below, the list of all known
+    // expressions.)
 
     private static final String KEY_PREF_DEGREE_MODE = "degree_mode";
+    private static final String KEY_PREF_SAVED_INDEX = "saved_index";
+    private static final String KEY_PREF_MEMORY_INDEX = "memory_index";
+    private static final String KEY_PREF_SAVED_NAME = "saved_name";
 
     // The minimum number of extra digits we always try to compute to improve the chance of
     // producing a correctly-rounded-towards-zero result.  The extra digits can be displayed to
@@ -130,68 +240,120 @@
     // estimating exponent size for truncating short representation.
     private static final int EXP_COST = 3;
 
-    private final Calculator mCalculator;
-    private final CalculatorResult mResult;
-
-    // The current caluclator expression.
-    private CalculatorExpr mExpr;
-
-    // Last saved expression.  Either null or contains a single CalculatorExpr.PreEval node.
-    private CalculatorExpr mSaved;
+    private final Activity mActivity;
 
     //  A hopefully unique name associated with mSaved.
     private String mSavedName;
 
-    // The expression may have changed since the last evaluation in ways that would affect its
+    // The main expression may have changed since the last evaluation in ways that would affect its
     // value.
     private boolean mChangedValue;
 
     // The main expression contains trig functions.
     private boolean mHasTrigFuncs;
 
-    private SharedPreferences mSharedPrefs;
+    public static final int INVALID_MSD = Integer.MAX_VALUE;
 
-    private boolean mDegreeMode;       // Currently in degree (not radian) mode.
+    /**
+     * An individual CalculatorExpr, together with its evaluation state.
+     * Only the main expression may be changed in-place.
+     * All other expressions are only added and never removed. The expressions themselves are
+     * never modified.
+     * All fields other than mExpr and mVal are touched only by the UI thread.
+     * For MAIN_INDEX, mExpr and mVal may change, but are also only ever touched by the UI thread.
+     * For all other expressions, mExpr does not change once the ExprInfo has been (atomically)
+     * added to mExprs. mVal may be asynchronously set by any thread, but we take care that it
+     * does not change after that. mDegreeMode is handled exactly like mExpr.
+     */
+    private class ExprInfo {
+        public CalculatorExpr mExpr;  // The expression itself.
+        public boolean mDegreeMode;  // Evaluating in degree, not radian, mode.
+        public ExprInfo(CalculatorExpr expr, boolean dm) {
+            mExpr = expr;
+            mDegreeMode = dm;
+            mVal = new AtomicReference<UnifiedReal>();
+        }
+
+        // Currently running expression evaluator, if any.  This is an AsyncEvaluator if
+        // mVal.get() == null, an AsyncReevaluator otherwise.
+        public AsyncTask mEvaluator;
+
+        // The remaining fields are valid only if an evaluation completed successfully.
+        // mVal always points to an AtomicReference, but that may be null.
+        public AtomicReference<UnifiedReal> mVal;
+        // We cache the best known decimal result in mResultString.  Whenever that is
+        // non-null, it is computed to exactly mResultStringOffset, which is always > 0.
+        // Valid only if mResultString is non-null and (for the main expression) !mChangedValue.
+        public String mResultString;
+        public int mResultStringOffset = 0;
+        // Number of digits to which (possibly incomplete) evaluation has been requested.
+        // Only accessed by UI thread.
+        public int mResultStringOffsetReq = 0;
+        // Position of most significant digit in current cached result, if determined.  This is just
+        // the index in mResultString holding the msd.
+        public int mMsdIndex = INVALID_MSD;
+        // Long timeout needed for evaluation?
+        public boolean mLongTimeout = false;
+        public long mTimeStamp;
+        public int mUtcOffset;
+    }
+
+    private ConcurrentHashMap<Long, ExprInfo> mExprs = new ConcurrentHashMap<Long, ExprInfo>();
+
+    // The database holding persistent expressions.
+    private ExpressionDB mExprDB;
+
+    private ExprInfo mMainExpr;  //  == mExprs.get(MAIN_INDEX)
+
+    private SharedPreferences mSharedPrefs;
 
     private final Handler mTimeoutHandler;  // Used to schedule evaluation timeouts.
 
-    // The following are valid only if an evaluation completed successfully.
-        private UnifiedReal mVal;               // Value of mExpr as UnifiedReal.
+    private void setMainExpr(ExprInfo expr) {
+        mMainExpr = expr;
+        mExprs.put(MAIN_INDEX, expr);
+    }
 
-    // We cache the best known decimal result in mResultString.  Whenever that is
-    // non-null, it is computed to exactly mResultStringOffset, which is always > 0.
-    // The cache is filled in by the UI thread.
-    // Valid only if mResultString is non-null and !mChangedValue.
-    private String mResultString;
-    private int mResultStringOffset = 0;
-
-    // Number of digits to which (possibly incomplete) evaluation has been requested.
-    // Only accessed by UI thread.
-    private int mResultStringOffsetReq;  // Number of digits that have been
-
-    public static final int INVALID_MSD = Integer.MAX_VALUE;
-
-    // Position of most significant digit in current cached result, if determined.  This is just
-    // the index in mResultString holding the msd.
-    private int mMsdIndex = INVALID_MSD;
-
-    // Currently running expression evaluator, if any.
-    private AsyncEvaluator mEvaluator;
-
-    // The one and only un-cancelled and currently running reevaluator. Touched only by UI thread.
-    private AsyncReevaluator mCurrentReevaluator;
-
-    Evaluator(Calculator calculator,
-              CalculatorResult resultDisplay) {
-        mCalculator = calculator;
-        mResult = resultDisplay;
-        mExpr = new CalculatorExpr();
-        mSaved = new CalculatorExpr();
+    Evaluator(Calculator calculator) {
+        mActivity = calculator;
+        setMainExpr(new ExprInfo(new CalculatorExpr(), false));
         mSavedName = "none";
         mTimeoutHandler = new Handler();
 
+        mExprDB = new ExpressionDB(mActivity);
         mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(calculator);
-        mDegreeMode = mSharedPrefs.getBoolean(KEY_PREF_DEGREE_MODE, false);
+        mMainExpr.mDegreeMode = mSharedPrefs.getBoolean(KEY_PREF_DEGREE_MODE, false);
+        long savedIndex = mSharedPrefs.getLong(KEY_PREF_SAVED_INDEX, 0L);
+        long memoryIndex = mSharedPrefs.getLong(KEY_PREF_MEMORY_INDEX, 0L);
+        if (savedIndex != 0) {
+            setSavedIndexWhenEvaluated(savedIndex);
+        }
+        if (memoryIndex != 0) {
+            setMemoryIndexWhenEvaluated(memoryIndex, false /* no need to persist again */);
+        }
+        mSavedName = mSharedPrefs.getString(KEY_PREF_SAVED_NAME, "none");
+    }
+
+    /**
+     * Retrieve minimum expression index.
+     * This is the minimum over all expressions, including uncached ones residing only
+     * in the data base. If no expressions with negative indices were preserved, this will
+     * return a small negative predefined constant.
+     * May be called from any thread, but will block until the database is opened.
+     */
+    public long getMinIndex() {
+        return mExprDB.getMinIndex();
+    }
+
+    /**
+     * Retrieve maximum expression index.
+     * This is the maximum over all expressions, including uncached ones residing only
+     * in the data base. If no expressions with positive indices were preserved, this will
+     * return 0.
+     * May be called from any thread, but will block until the database is opened.
+     */
+    public long getMaxIndex() {
+        return mExprDB.getMaxIndex();
     }
 
     /**
@@ -224,7 +386,7 @@
     }
 
     private void displayCancelledMessage() {
-        AlertDialogFragment.showMessageDialog(mCalculator, 0, R.string.cancelled, 0);
+        AlertDialogFragment.showMessageDialog(mActivity, 0, R.string.cancelled, 0);
     }
 
     // Timeout handling.
@@ -234,16 +396,6 @@
     // destined to fail.
 
     /**
-     * Is a long timeout in effect for the main expression?
-     */
-    private boolean mLongTimeout = false;
-
-    /**
-     * Is a long timeout in effect for the saved expression?
-     */
-    private boolean mLongSavedTimeout = false;
-
-    /**
      * Return the timeout in milliseconds.
      * @param longTimeout a long timeout is in effect
      */
@@ -258,27 +410,27 @@
      * @param longTimeout a long timeout is in effect
      */
     private int getMaxResultBits(boolean longTimeout) {
-        return longTimeout ? 350000 : 120000;
+        return longTimeout ? 700000 : 240000;
     }
 
     /**
      * Timeout for unrequested, speculative evaluations, in milliseconds.
      */
-    private final long QUICK_TIMEOUT = 1000;
+    private static final long QUICK_TIMEOUT = 1000;
 
     /**
      * Maximum result bit length for unrequested, speculative evaluations.
      * Also used to bound evaluation precision for small non-zero fractions.
      */
-    private final int QUICK_MAX_RESULT_BITS = 50000;
+    private static final int QUICK_MAX_RESULT_BITS = 50000;
 
-    private void displayTimeoutMessage() {
-        AlertDialogFragment.showMessageDialog(mCalculator, R.string.dialog_timeout,
-                R.string.timeout, mLongTimeout ? 0 : R.string.ok_remove_timeout);
+    private void displayTimeoutMessage(boolean longTimeout) {
+        AlertDialogFragment.showMessageDialog(mActivity, R.string.dialog_timeout,
+                R.string.timeout, longTimeout ? 0 : R.string.ok_remove_timeout);
     }
 
-    public void setLongTimeOut() {
-        mLongTimeout = true;
+    public void setLongTimeout() {
+        mMainExpr.mLongTimeout = true;
     }
 
     /**
@@ -292,49 +444,80 @@
         private boolean mRequired; // Result was requested by user.
         private boolean mQuiet;  // Suppress cancellation message.
         private Runnable mTimeoutRunnable = null;
-        AsyncEvaluator(boolean dm, boolean required) {
+        private EvaluationListener mListener;  // Completion callback.
+        private CharMetricsInfo mCharMetricsInfo;  // Where to get result size information.
+        private long mIndex;  //  Expression index.
+        private ExprInfo mExprInfo;  // Current expression.
+
+        AsyncEvaluator(long index, EvaluationListener listener, CharMetricsInfo cmi, boolean dm,
+                boolean required) {
+            mIndex = index;
+            mListener = listener;
+            mCharMetricsInfo = cmi;
             mDm = dm;
             mRequired = required;
-            mQuiet = !required;
+            mQuiet = !required || mIndex != MAIN_INDEX;
+            mExprInfo = mExprs.get(mIndex);
+            if (mExprInfo.mEvaluator != null) {
+                throw new AssertionError("Evaluation already in progress!");
+            }
         }
-        private void handleTimeOut() {
+
+        private void handleTimeout() {
+            // Runs in UI thread.
             boolean running = (getStatus() != AsyncTask.Status.FINISHED);
             if (running && cancel(true)) {
-                mEvaluator = null;
-                // Replace mExpr with clone to avoid races if task
-                // still runs for a while.
-                mExpr = (CalculatorExpr)mExpr.clone();
-                if (mRequired) {
+                mExprs.get(mIndex).mEvaluator = null;
+                if (mRequired && mIndex == MAIN_INDEX) {
+                    // Replace mExpr with clone to avoid races if task still runs for a while.
+                    mMainExpr.mExpr = (CalculatorExpr)mMainExpr.mExpr.clone();
                     suppressCancelMessage();
-                    displayTimeoutMessage();
+                    displayTimeoutMessage(mExprInfo.mLongTimeout);
                 }
             }
         }
+
         private void suppressCancelMessage() {
             mQuiet = true;
         }
+
         @Override
         protected void onPreExecute() {
-            long timeout = mRequired ? getTimeout(mLongTimeout) : QUICK_TIMEOUT;
+            long timeout = mRequired ? getTimeout(mExprInfo.mLongTimeout) : QUICK_TIMEOUT;
+            if (mIndex != MAIN_INDEX) {
+                // We evaluated the expression before with the current timeout, so this shouldn't
+                // ever time out. We evaluate it with a ridiculously long timeout to avoid running
+                // down the battery if something does go wrong. But we only log such timeouts, and
+                // invoke the listener with onCancelled.
+                timeout *= 10;
+            }
             mTimeoutRunnable = new Runnable() {
                 @Override
                 public void run() {
-                    handleTimeOut();
+                    handleTimeout();
                 }
             };
             mTimeoutHandler.postDelayed(mTimeoutRunnable, timeout);
         }
+
         /**
          * Is a computed result too big for decimal conversion?
          */
         private boolean isTooBig(UnifiedReal res) {
-            int maxBits = mRequired ? getMaxResultBits(mLongTimeout) : QUICK_MAX_RESULT_BITS;
+            int maxBits = mRequired ? getMaxResultBits(mExprInfo.mLongTimeout)
+                    : QUICK_MAX_RESULT_BITS;
             return res.approxWholeNumberBitsGreaterThan(maxBits);
         }
+
         @Override
         protected InitialResult doInBackground(Void... nothing) {
             try {
-                UnifiedReal res = mExpr.eval(mDm);
+                // mExpr does not change while we are evaluating; thus it's OK to read here.
+                UnifiedReal res = mExprInfo.mVal.get();
+                if (res == null) {
+                    res = mExprInfo.mExpr.eval(mDm, Evaluator.this);
+                    res = putResultIfAbsent(mIndex, res);
+                }
                 if (isTooBig(res)) {
                     // Avoid starting a long uninterruptible decimal conversion.
                     return new InitialResult(R.string.timeout);
@@ -361,7 +544,8 @@
                     }
                 }
                 final int lsdOffset = getLsdOffset(res, initResult, initResult.indexOf('.'));
-                final int initDisplayOffset = getPreferredPrec(initResult, msd, lsdOffset);
+                final int initDisplayOffset = getPreferredPrec(initResult, msd, lsdOffset,
+                        mCharMetricsInfo);
                 final int newPrecOffset = initDisplayOffset + EXTRA_DIGITS;
                 if (newPrecOffset > precOffset) {
                     precOffset = newPrecOffset;
@@ -381,50 +565,56 @@
                 return new InitialResult(R.string.error_aborted);
             }
         }
+
         @Override
         protected void onPostExecute(InitialResult result) {
-            mEvaluator = null;
+            mExprInfo.mEvaluator = null;
             mTimeoutHandler.removeCallbacks(mTimeoutRunnable);
             if (result.isError()) {
                 if (result.errorResourceId == R.string.timeout) {
-                    if (mRequired) {
-                        displayTimeoutMessage();
+                    // Emulating timeout due to large result.
+                    if (mRequired && mIndex == MAIN_INDEX) {
+                        displayTimeoutMessage(mExprs.get(mIndex).mLongTimeout);
                     }
-                    mCalculator.onCancelled();
+                    mListener.onCancelled(mIndex);
                 } else {
-                    mCalculator.onError(result.errorResourceId);
+                    mListener.onError(mIndex, result.errorResourceId);
                 }
                 return;
             }
-            mVal = result.val;
-            mResultString = result.newResultString;
-            mResultStringOffset = result.newResultStringOffset;
-            final int dotIndex = mResultString.indexOf('.');
-            String truncatedWholePart = mResultString.substring(0, dotIndex);
+            // mExprInfo.mVal was already set asynchrously by child thread.
+            mExprInfo.mResultString = result.newResultString;
+            mExprInfo.mResultStringOffset = result.newResultStringOffset;
+            final int dotIndex = mExprInfo.mResultString.indexOf('.');
+            String truncatedWholePart = mExprInfo.mResultString.substring(0, dotIndex);
             // Recheck display precision; it may change, since display dimensions may have been
             // unknow the first time.  In that case the initial evaluation precision should have
             // been conservative.
             // TODO: Could optimize by remembering display size and checking for change.
             int initPrecOffset = result.initDisplayOffset;
-            final int msdIndex = getMsdIndexOf(mResultString);
-            final int leastDigOffset = getLsdOffset(mVal, mResultString, dotIndex);
-            final int newInitPrecOffset = getPreferredPrec(mResultString, msdIndex, leastDigOffset);
+            mExprInfo.mMsdIndex = getMsdIndexOf(mExprInfo.mResultString);
+            final int leastDigOffset = getLsdOffset(result.val, mExprInfo.mResultString,
+                    dotIndex);
+            final int newInitPrecOffset = getPreferredPrec(mExprInfo.mResultString,
+                    mExprInfo.mMsdIndex, leastDigOffset, mCharMetricsInfo);
             if (newInitPrecOffset < initPrecOffset) {
                 initPrecOffset = newInitPrecOffset;
             } else {
                 // They should be equal.  But nothing horrible should happen if they're not. e.g.
                 // because CalculatorResult.MAX_WIDTH was too small.
             }
-            mCalculator.onEvaluate(initPrecOffset, msdIndex, leastDigOffset, truncatedWholePart);
+            mListener.onEvaluate(mIndex, initPrecOffset, mExprInfo.mMsdIndex, leastDigOffset,
+                    truncatedWholePart);
         }
+
         @Override
         protected void onCancelled(InitialResult result) {
             // Invoker resets mEvaluator.
             mTimeoutHandler.removeCallbacks(mTimeoutRunnable);
-            if (mRequired && !mQuiet) {
+            if (!mQuiet) {
                 displayCancelledMessage();
             } // Otherwise, if mRequired, timeout processing displayed message.
-            mCalculator.onCancelled();
+            mListener.onCancelled(mIndex);
             // Just drop the evaluation; Leave expression displayed.
             return;
         }
@@ -478,13 +668,26 @@
      * Compute new mResultString contents to prec digits to the right of the decimal point.
      * Ensure that onReevaluate() is called after doing so.  If the evaluation fails for reasons
      * other than a timeout, ensure that onError() is called.
+     * This assumes that initial evaluation of the expression has been successfully
+     * completed.
      */
     private class AsyncReevaluator extends AsyncTask<Integer, Void, ReevalResult> {
+        private long mIndex;  // Index of expression to evaluate.
+        private EvaluationListener mListener;
+        private ExprInfo mExprInfo;
+
+        AsyncReevaluator(long index, EvaluationListener listener) {
+            mIndex = index;
+            mListener = listener;
+            mExprInfo = mExprs.get(mIndex);
+        }
+
         @Override
         protected ReevalResult doInBackground(Integer... prec) {
             try {
                 final int precOffset = prec[0].intValue();
-                return new ReevalResult(mVal.toStringTruncated(precOffset), precOffset);
+                return new ReevalResult(mExprInfo.mVal.get().toStringTruncated(precOffset),
+                        precOffset);
             } catch(ArithmeticException e) {
                 return null;
             } catch(CR.PrecisionOverflowException e) {
@@ -502,39 +705,43 @@
                 // This should only be possible in the extremely rare case of encountering a
                 // domain error while reevaluating or in case of a precision overflow.  We don't
                 // know of a way to get the latter with a plausible amount of user input.
-                mCalculator.onError(R.string.error_nan);
+                // FIXME: Doesn't currently work. Didn't work in earlier releases either.
+                mListener.onError(mIndex, R.string.error_nan);
             } else {
-                if (result.newResultStringOffset < mResultStringOffset) {
+                if (result.newResultStringOffset < mExprInfo.mResultStringOffset) {
                     throw new AssertionError("Unexpected onPostExecute timing");
                 }
-                mResultString = unflipZeroes(mResultString, mResultStringOffset,
-                        result.newResultString, result.newResultStringOffset);
-                mResultStringOffset = result.newResultStringOffset;
-                mCalculator.onReevaluate();
+                mExprInfo.mResultString = unflipZeroes(mExprInfo.mResultString,
+                        mExprInfo.mResultStringOffset, result.newResultString,
+                        result.newResultStringOffset);
+                mExprInfo.mResultStringOffset = result.newResultStringOffset;
+                mListener.onReevaluate(mIndex);
             }
-            mCurrentReevaluator = null;
+            mExprInfo.mEvaluator = null;
         }
         // On cancellation we do nothing; invoker should have left no trace of us.
     }
 
     /**
-     * If necessary, start an evaluation to precOffset.
-     * Ensure that the display is redrawn when it completes.
+     * If necessary, start an evaluation of the expression at the given index to precOffset.
+     * If we start an evaluation the listener is notified on completion.
      */
-    private void ensureCachePrec(int precOffset) {
-        if (mResultString != null && mResultStringOffset >= precOffset
-                || mResultStringOffsetReq >= precOffset) return;
-        if (mCurrentReevaluator != null) {
+    private void ensureCachePrec(long index, int precOffset, EvaluationListener listener) {
+        ExprInfo ei = mExprs.get(index);
+        if (ei.mResultString != null && ei.mResultStringOffset >= precOffset
+                || ei.mResultStringOffsetReq >= precOffset) return;
+        if (ei.mEvaluator != null) {
             // Ensure we only have one evaluation running at a time.
-            mCurrentReevaluator.cancel(true);
-            mCurrentReevaluator = null;
+            ei.mEvaluator.cancel(true);
+            ei.mEvaluator = null;
         }
-        mCurrentReevaluator = new AsyncReevaluator();
-        mResultStringOffsetReq = precOffset + PRECOMPUTE_DIGITS;
-        if (mResultString != null) {
-            mResultStringOffsetReq += mResultStringOffsetReq / PRECOMPUTE_DIVISOR;
+        AsyncReevaluator reEval = new AsyncReevaluator(index, listener);
+        ei.mEvaluator = reEval;
+        ei.mResultStringOffsetReq = precOffset + PRECOMPUTE_DIGITS;
+        if (ei.mResultString != null) {
+            ei.mResultStringOffsetReq += ei.mResultStringOffsetReq / PRECOMPUTE_DIVISOR;
         }
-        mCurrentReevaluator.execute(mResultStringOffsetReq);
+        reEval.execute(ei.mResultStringOffsetReq);
     }
 
     /**
@@ -546,7 +753,7 @@
      *         Integer.MIN_VALUE if we cannot determine.  Integer.MAX_VALUE if there is no lsd,
      *         or we cannot determine it.
      */
-    int getLsdOffset(UnifiedReal val, String cache, int decIndex) {
+    static int getLsdOffset(UnifiedReal val, String cache, int decIndex) {
         if (val.definitelyZero()) return Integer.MIN_VALUE;
         int result = val.digitsRequired();
         if (result == 0) {
@@ -570,12 +777,13 @@
      * @param lastDigitOffset Position of least significant digit (1 = tenths digit)
      *                  or Integer.MAX_VALUE.
      */
-    private int getPreferredPrec(String cache, int msd, int lastDigitOffset) {
-        final int lineLength = mResult.getMaxChars();
+    private static int getPreferredPrec(String cache, int msd, int lastDigitOffset,
+            CharMetricsInfo cm) {
+        final int lineLength = cm.getMaxChars();
         final int wholeSize = cache.indexOf('.');
-        final float rawSepChars = mResult.separatorChars(cache, wholeSize);
-        final float rawSepCharsNoDecimal = rawSepChars - mResult.getNoEllipsisCredit();
-        final float rawSepCharsWithDecimal = rawSepCharsNoDecimal - mResult.getDecimalCredit();
+        final float rawSepChars = cm.separatorChars(cache, wholeSize);
+        final float rawSepCharsNoDecimal = rawSepChars - cm.getNoEllipsisCredit();
+        final float rawSepCharsWithDecimal = rawSepCharsNoDecimal - cm.getDecimalCredit();
         final int sepCharsNoDecimal = (int) Math.ceil(Math.max(rawSepCharsNoDecimal, 0.0f));
         final int sepCharsWithDecimal = (int) Math.ceil(Math.max(rawSepCharsWithDecimal, 0.0f));
         final int negative = cache.charAt(0) == '-' ? 1 : 0;
@@ -637,7 +845,7 @@
      * @param lsdOffset Position of least significant digit in finite representation,
      *            relative to decimal point, or MAX_VALUE.
      */
-    private String getShortString(String cache, int msdIndex, int lsdOffset) {
+    private static String getShortString(String cache, int msdIndex, int lsdOffset) {
         // This somewhat mirrors the display formatting code, but
         // - The constants are different, since we don't want to use the whole display.
         // - This is an easier problem, since we don't support scrolling and the length
@@ -732,25 +940,26 @@
     }
 
     /**
-     * Return most significant digit index in the currently computed result.
+     * Return most significant digit index for the result of the expressin at the given index.
      * Returns an index in the result character array.  Return INVALID_MSD if the current result
      * is too close to zero to determine the result.
      * Result is almost consistent through reevaluations: It may increase by one, once.
      */
-    private int getMsdIndex() {
-        if (mMsdIndex != INVALID_MSD) {
+    private int getMsdIndex(long index) {
+        ExprInfo ei = mExprs.get(index);
+        if (ei.mMsdIndex != INVALID_MSD) {
             // 0.100000... can change to 0.0999999...  We may have to correct once by one digit.
-            if (mResultString.charAt(mMsdIndex) == '0') {
-                mMsdIndex++;
+            if (ei.mResultString.charAt(ei.mMsdIndex) == '0') {
+                ei.mMsdIndex++;
             }
-            return mMsdIndex;
+            return ei.mMsdIndex;
         }
-        if (mVal.definitelyZero()) {
+        if (ei.mVal.get().definitelyZero()) {
             return INVALID_MSD;  // None exists
         }
         int result = INVALID_MSD;
-        if (mResultString != null) {
-            result = getMsdIndexOf(mResultString);
+        if (ei.mResultString != null) {
+            result = ei.mMsdIndex = getMsdIndexOf(ei.mResultString);
         }
         return result;
     }
@@ -763,7 +972,7 @@
      * Return result to precOffset[0] digits to the right of the decimal point.
      * PrecOffset[0] is updated if the original value is out of range.  No exponent or other
      * indication of precision is added.  The result is returned immediately, based on the current
-     * cache contents, but it may contain question marks for unknown digits.  It may also use
+     * cache contents, but it may contain blanks for unknown digits.  It may also use
      * uncertain digits within EXTRA_DIGITS.  If either of those occurred, schedule a reevaluation
      * and redisplay operation.  Uncertain digits never appear to the left of the decimal point.
      * PrecOffset[0] may be negative to only retrieve digits to the left of the decimal point.
@@ -774,32 +983,35 @@
      * Result uses US conventions; is NOT internationalized.  Use getResult() and UnifiedReal
      * operations to determine whether the result is exact, or whether we dropped trailing digits.
      *
+     * @param index Index of expression to approximate
      * @param precOffset Zeroth element indicates desired and actual precision
      * @param maxPrecOffset Maximum adjusted precOffset[0]
      * @param maxDigs Maximum length of result
      * @param truncated Zeroth element is set if leading nonzero digits were dropped
      * @param negative Zeroth element is set of the result is negative.
+     * @param listener EvaluationListener to notify when reevaluation is complete.
      */
-    public String getString(int[] precOffset, int maxPrecOffset, int maxDigs, boolean[] truncated,
-            boolean[] negative) {
+    public String getString(long index, int[] precOffset, int maxPrecOffset, int maxDigs,
+            boolean[] truncated, boolean[] negative, EvaluationListener listener) {
+        ExprInfo ei = mExprs.get(index);
         int currentPrecOffset = precOffset[0];
         // Make sure we eventually get a complete answer
-        if (mResultString == null) {
-            ensureCachePrec(currentPrecOffset + EXTRA_DIGITS);
+        if (ei.mResultString == null) {
+            ensureCachePrec(index, currentPrecOffset + EXTRA_DIGITS, listener);
             // Nothing else to do now; seems to happen on rare occasion with weird user input
             // timing; Will repair itself in a jiffy.
             return " ";
         } else {
-            ensureCachePrec(currentPrecOffset + EXTRA_DIGITS + mResultString.length()
-                    / EXTRA_DIVISOR);
+            ensureCachePrec(index, currentPrecOffset + EXTRA_DIGITS + ei.mResultString.length()
+                    / EXTRA_DIVISOR, listener);
         }
         // Compute an appropriate substring of mResultString.  Pad if necessary.
-        final int len = mResultString.length();
-        final boolean myNegative = mResultString.charAt(0) == '-';
+        final int len = ei.mResultString.length();
+        final boolean myNegative = ei.mResultString.charAt(0) == '-';
         negative[0] = myNegative;
         // Don't scroll left past leftmost digits in mResultString unless that still leaves an
         // integer.
-            int integralDigits = len - mResultStringOffset;
+            int integralDigits = len - ei.mResultStringOffset;
                             // includes 1 for dec. pt
             if (myNegative) {
                 --integralDigits;
@@ -808,19 +1020,19 @@
             currentPrecOffset = Math.min(Math.max(currentPrecOffset, minPrecOffset),
                     maxPrecOffset);
             precOffset[0] = currentPrecOffset;
-        int extraDigs = mResultStringOffset - currentPrecOffset; // trailing digits to drop
+        int extraDigs = ei.mResultStringOffset - currentPrecOffset; // trailing digits to drop
         int deficit = 0;  // The number of digits we're short
         if (extraDigs < 0) {
             extraDigs = 0;
-            deficit = Math.min(currentPrecOffset - mResultStringOffset, maxDigs);
+            deficit = Math.min(currentPrecOffset - ei.mResultStringOffset, maxDigs);
         }
         int endIndex = len - extraDigs;
         if (endIndex < 1) {
             return " ";
         }
         int startIndex = Math.max(endIndex + deficit - maxDigs, 0);
-        truncated[0] = (startIndex > getMsdIndex());
-        String result = mResultString.substring(startIndex, endIndex);
+        truncated[0] = (startIndex > getMsdIndex(index));
+        String result = ei.mResultString.substring(startIndex, endIndex);
         if (deficit > 0) {
             result += StringUtils.repeat(' ', deficit);
             // Blank character is replaced during translation.
@@ -832,135 +1044,169 @@
     }
 
     /**
-     * Return rational representation of current result, if any.
-     * Return null if the result is irrational, or we couldn't track the rational value,
-     * e.g. because the denominator got too big.
+     * Clear the cache for the main expression.
      */
-    public UnifiedReal getResult() {
-        return mVal;
-    }
-
-    private void clearCache() {
-        mResultString = null;
-        mResultStringOffset = mResultStringOffsetReq = 0;
-        mMsdIndex = INVALID_MSD;
+    private void clearMainCache() {
+        mMainExpr.mVal.set(null);
+        mMainExpr.mResultString = null;
+        mMainExpr.mResultStringOffset = mMainExpr.mResultStringOffsetReq = 0;
+        mMainExpr.mMsdIndex = INVALID_MSD;
     }
 
 
-    private void clearPreservingTimeout() {
-        mExpr.clear();
+    private void clearMainPreservingTimeout() {
+        mMainExpr.mExpr.clear();
         mHasTrigFuncs = false;
-        clearCache();
+        clearMainCache();
     }
 
-    public void clear() {
-        clearPreservingTimeout();
-        mLongTimeout = false;
+    public void clearMain() {
+        clearMainPreservingTimeout();
+        mMainExpr.mLongTimeout = false;
+    }
+
+    public void clearEverything() {
+        boolean dm = mMainExpr.mDegreeMode;
+        cancelAll(true);
+        setSavedIndex(0);
+        setMemoryIndex(0);
+        mExprDB.eraseAll();
+        mExprs.clear();
+        setMainExpr(new ExprInfo(new CalculatorExpr(), dm));
     }
 
     /**
-     * Start asynchronous result evaluation of formula.
-     * Will result in display on completion.
+     * Start asynchronous evaluation.
+     * Invoke listener on successful completion. If the result is required, invoke
+     * onCancelled() if cancelled.
+     * @param index index of expression to be evaluated.
      * @param required result was explicitly requested by user.
      */
-    private void evaluateResult(boolean required) {
-        clearCache();
-        mEvaluator = new AsyncEvaluator(mDegreeMode, required);
-        mEvaluator.execute();
-        mChangedValue = false;
-    }
-
-    /**
-     * Start optional evaluation of result and display when ready.
-     * Can quietly time out without a user-visible display.
-     */
-    public void evaluateAndShowResult() {
-        if (!mChangedValue) {
-            // Already done or in progress.
-            return;
+    private void evaluateResult(long index, EvaluationListener listener, CharMetricsInfo cmi,
+            boolean required) {
+        ExprInfo ei = mExprs.get(index);
+        clearMainCache();
+        AsyncEvaluator eval =  new AsyncEvaluator(index, listener, cmi, ei.mDegreeMode, required);
+        ei.mEvaluator = eval;
+        eval.execute();
+        if (index == MAIN_INDEX) {
+            mChangedValue = false;
         }
-        // In very odd cases, there can be significant latency to evaluate.
-        // Don't show obsolete result.
-        mResult.clear();
-        evaluateResult(false);
     }
 
     /**
-     * Start required evaluation of result and display when ready.
-     * Will eventually call back mCalculator to display result or error, or display
-     * a timeout message.  Uses longer timeouts than optional evaluation.
+     * Notify listener of a previously completed evaluation.
      */
-    public void requireResult() {
-        if (mResultString == null || mChangedValue) {
-            // Restart evaluator in requested mode, i.e. with longer timeout.
-            cancelAll(true);
-            evaluateResult(true);
+    void notifyImmediately(long index, ExprInfo ei, EvaluationListener listener,
+            CharMetricsInfo cmi) {
+        final int dotIndex = ei.mResultString.indexOf('.');
+        final String truncatedWholePart = ei.mResultString.substring(0, dotIndex);
+        final int leastDigOffset = getLsdOffset(ei.mVal.get(), ei.mResultString, dotIndex);
+        final int msdIndex = getMsdIndex(index);
+        final int preferredPrecOffset = getPreferredPrec(ei.mResultString, msdIndex,
+                leastDigOffset, cmi);
+        listener.onEvaluate(index, preferredPrecOffset, msdIndex, leastDigOffset,
+                truncatedWholePart);
+    }
+
+    /**
+     * Start optional evaluation of expression and display when ready.
+     * @param index of expression to be evaluated.
+     * Can quietly time out without a listener callback.
+     */
+    public void evaluateAndNotify(long index, EvaluationListener listener, CharMetricsInfo cmi) {
+        if (index == MAIN_INDEX) {
+            if (mMainExpr.mResultString != null && !mChangedValue) {
+                // Already done. Just notify.
+                notifyImmediately(MAIN_INDEX, mMainExpr, listener, cmi);
+                return;
+            }
         } else {
-            // Notify immediately, reusing existing result.
-            final int dotIndex = mResultString.indexOf('.');
-            final String truncatedWholePart = mResultString.substring(0, dotIndex);
-            final int leastDigOffset = getLsdOffset(mVal, mResultString, dotIndex);
-            final int msdIndex = getMsdIndex();
-            final int preferredPrecOffset = getPreferredPrec(mResultString, msdIndex,
-                    leastDigOffset);
-            mCalculator.onEvaluate(preferredPrecOffset, msdIndex, leastDigOffset,
-                    truncatedWholePart);
+            ensureExprIsCached(index);
+        }
+        evaluateResult(index, listener, cmi, false);
+    }
+
+    /**
+     * Start required evaluation of expression at given index and call back listener when ready.
+     * If index is MAIN_EXPR, we may also directly display a timeout message.
+     * Uses longer timeouts than optional evaluation.
+     */
+    public void requireResult(long index, EvaluationListener listener, CharMetricsInfo cmi) {
+        ExprInfo ei = ensureExprIsCached(index);
+        if (ei.mResultString == null || (index == MAIN_INDEX && mChangedValue)) {
+            // Restart evaluator in requested mode, i.e. with longer timeout.
+            cancel(ei, true);
+            evaluateResult(index, listener, cmi, true);
+        } else {
+            notifyImmediately(index, ei, listener, cmi);
         }
     }
 
     /**
      * Is a reevaluation still in progress?
      */
-    public boolean reevaluationInProgress() {
-        return mCurrentReevaluator != null;
+    public boolean evaluationInProgress(long index) {
+        ExprInfo ei = mExprs.get(index);
+        return ei != null && ei.mEvaluator != null;
     }
 
     /**
      * Cancel all current background tasks.
      * @param quiet suppress cancellation message
-     * @return      true if we cancelled an initial evaluation
+     * @return true if we cancelled an initial evaluation
      */
-    public boolean cancelAll(boolean quiet) {
-        if (mCurrentReevaluator != null) {
-            mCurrentReevaluator.cancel(true);
-            mResultStringOffsetReq = mResultStringOffset;
-            // Backgound computation touches only constructive reals.
-            // OK not to wait.
-            mCurrentReevaluator = null;
-        }
-        if (mEvaluator != null) {
-            if (quiet) {
-                mEvaluator.suppressCancelMessage();
+    private boolean cancel(ExprInfo expr, boolean quiet) {
+        if (expr.mEvaluator != null) {
+            // Reevaluation in progress.
+            if (expr.mVal != null) {
+                expr.mEvaluator.cancel(true);
+                expr.mResultStringOffsetReq = expr.mResultStringOffset;
+                // Backgound computation touches only constructive reals.
+                // OK not to wait.
+                expr.mEvaluator = null;
+            } else {
+                if (quiet) {
+                    ((AsyncEvaluator)(expr.mEvaluator)).suppressCancelMessage();
+                }
+                expr.mEvaluator.cancel(true);
+                if (expr == mMainExpr) {
+                    // The expression is modifiable, and the AsyncTask is reading it.
+                    // There seems to be no good way to wait for cancellation.
+                    // Give ourselves a new copy to work on instead.
+                    mMainExpr.mExpr = (CalculatorExpr)mMainExpr.mExpr.clone();
+                    // Approximation of constructive reals should be thread-safe,
+                    // so we can let that continue until it notices the cancellation.
+                    mChangedValue = true;    // Didn't do the expected evaluation.
+                }
+                expr.mEvaluator = null;
+                return true;
             }
-            mEvaluator.cancel(true);
-            // There seems to be no good way to wait for cancellation
-            // to complete, and the evaluation continues to look at
-            // mExpr, which we will again modify.
-            // Give ourselves a new copy to work on instead.
-            mExpr = (CalculatorExpr)mExpr.clone();
-            // Approximation of constructive reals should be thread-safe,
-            // so we can let that continue until it notices the cancellation.
-            mEvaluator = null;
-            mChangedValue = true;    // Didn't do the expected evaluation.
-            return true;
         }
         return false;
     }
 
+    public void cancelAll(boolean quiet) {
+        // TODO: May want to keep active evaluators in a HashSet to avoid traversing
+        // all expressions we've looked at.
+        for (ExprInfo expr: mExprs.values()) {
+            cancel(expr, quiet);
+        }
+    }
+
     /**
      * Restore the evaluator state, including the expression and any saved value.
      */
     public void restoreInstanceState(DataInput in) {
         mChangedValue = true;
+        // FIXME: per our current discussion, this should also restore expressions that
+        // are referenced by the current expression to avoid database initialization
+        // latency on normal startup.
         try {
-            CalculatorExpr.initExprInput();
-            mDegreeMode = in.readBoolean();
-            mLongTimeout = in.readBoolean();
-            mLongSavedTimeout = in.readBoolean();
-            mExpr = new CalculatorExpr(in);
-            mSavedName = in.readUTF();
-            mSaved = new CalculatorExpr(in);
-            mHasTrigFuncs = mExpr.hasTrigFuncs();
+            mMainExpr.mDegreeMode = in.readBoolean();
+            mMainExpr.mLongTimeout = in.readBoolean();
+            mMainExpr.mExpr = new CalculatorExpr(in);
+            mHasTrigFuncs = hasTrigFuncs();
         } catch (IOException e) {
             Log.v("Calculator", "Exception while restoring:\n" + e);
         }
@@ -971,13 +1217,9 @@
      */
     public void saveInstanceState(DataOutput out) {
         try {
-            CalculatorExpr.initExprOutput();
-            out.writeBoolean(mDegreeMode);
-            out.writeBoolean(mLongTimeout);
-            out.writeBoolean(mLongSavedTimeout);
-            mExpr.write(out);
-            out.writeUTF(mSavedName);
-            mSaved.write(out);
+            out.writeBoolean(mMainExpr.mDegreeMode);
+            out.writeBoolean(mMainExpr.mLongTimeout);
+            mMainExpr.mExpr.write(out);
         } catch (IOException e) {
             Log.v("Calculator", "Exception while saving state:\n" + e);
         }
@@ -985,7 +1227,7 @@
 
 
     /**
-     * Append a button press to the current expression.
+     * Append a button press to the main expression.
      * @param id Button identifier for the character or operator to be added.
      * @return false if we rejected the insertion due to obvious syntax issues, and the expression
      * is unchanged; true otherwise
@@ -996,7 +1238,7 @@
             return true;
         } else {
             mChangedValue = mChangedValue || !KeyMaps.isBinary(id);
-            if (mExpr.add(id)) {
+            if (mMainExpr.mExpr.add(id)) {
                 if (!mHasTrigFuncs) {
                     mHasTrigFuncs = KeyMaps.isTrigFunc(id);
                 }
@@ -1007,49 +1249,151 @@
         }
     }
 
+    /**
+     * Delete last taken from main expression.
+     */
     public void delete() {
         mChangedValue = true;
-        mExpr.delete();
-        if (mExpr.isEmpty()) {
-            mLongTimeout = false;
+        mMainExpr.mExpr.delete();
+        if (mMainExpr.mExpr.isEmpty()) {
+            mMainExpr.mLongTimeout = false;
         }
-        mHasTrigFuncs = mExpr.hasTrigFuncs();
+        mHasTrigFuncs = hasTrigFuncs();
     }
 
-    void setDegreeMode(boolean degreeMode) {
+    /**
+     * Set degree mode for main expression.
+     */
+    public void setDegreeMode(boolean degreeMode) {
         mChangedValue = true;
-        mDegreeMode = degreeMode;
+        mMainExpr.mDegreeMode = degreeMode;
 
         mSharedPrefs.edit()
                 .putBoolean(KEY_PREF_DEGREE_MODE, degreeMode)
                 .apply();
     }
 
-    boolean getDegreeMode() {
-        return mDegreeMode;
-    }
-
     /**
-     * @return the {@link CalculatorExpr} representation of the current result.
+     * Return an ExprInfo for a copy of the main expression.
+     * We remove trailing binary operators in the copy.
      */
-    private CalculatorExpr getResultExpr() {
-        final int dotIndex = mResultString.indexOf('.');
-        final int leastDigOffset = getLsdOffset(mVal, mResultString, dotIndex);
-        return mExpr.abbreviate(mVal, mDegreeMode,
-                getShortString(mResultString, getMsdIndexOf(mResultString), leastDigOffset));
+    private ExprInfo copy(long index, boolean copyValue) {
+        ExprInfo fromEi = mExprs.get(index);
+        ExprInfo ei = new ExprInfo((CalculatorExpr)fromEi.mExpr.clone(), fromEi.mDegreeMode);
+        while (ei.mExpr.hasTrailingBinary()) {
+            ei.mExpr.delete();
+        }
+        if (copyValue) {
+            ei.mVal = new AtomicReference<UnifiedReal>(fromEi.mVal.get());
+            ei.mResultString = fromEi.mResultString;
+            ei.mResultStringOffset = ei.mResultStringOffsetReq = fromEi.mResultStringOffset;
+            ei.mMsdIndex = fromEi.mMsdIndex;
+        }
+        ei.mLongTimeout = fromEi.mLongTimeout;
+        return ei;
     }
 
     /**
-     * Abbreviate the current expression to a pre-evaluated expression node.
+     * Return an ExprInfo corresponding to the sum of the expressions at the
+     * two indices.
+     * index2 should correspond to an immutable expression, and should thus NOT
+     * be MAIN_INDEX.  Both are presumed to have been previously evaluated.
+     * The result is unevaluated.
+     */
+    private ExprInfo sum(long index1, long index2) {
+        ExprInfo ei = copy(index1, false);
+        // ei is still private to us, so we can modify it.
+        ExprInfo other =  mExprs.get(index2);
+        ei.mLongTimeout |= other.mLongTimeout;
+        ei.mExpr.add(R.id.op_add);
+        ei.mExpr.append(getCollapsedExpr(index2));
+        return ei;
+    }
+
+    /**
+     * Add the expression described by the argument to the database.
+     * Returns the new row id in the database.
+     * If in_history is true, add it with a positive index, so it will appear in the history.
+     */
+    private long addToDB(boolean in_history, ExprInfo ei) {
+        /* TODO: Possibly do this in a different thread. */
+        ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
+        try (DataOutputStream out = new DataOutputStream(byteArrayStream)) {
+            ei.mExpr.write(out);
+        } catch (IOException e) {
+            // Impossible; No IO involved.
+            throw new AssertionError("Impossible IO exception", e);
+        }
+        byte[] serializedExpr = byteArrayStream.toByteArray();
+        ExpressionDB.RowData rd = new ExpressionDB.RowData(serializedExpr, ei.mDegreeMode,
+                ei.mLongTimeout, 0, 0);
+        long resultIndex = mExprDB.addRow(!in_history, rd);
+        if (mExprs.get(resultIndex) != null) {
+            throw new AssertionError("result slot already occupied! + Slot = " + resultIndex);
+        }
+        // Add newly assigned date to the cache.
+        ei.mTimeStamp = rd.mTimeStamp;
+        ei.mUtcOffset = rd.utcOffset();
+        mExprs.put(resultIndex, ei);
+        return resultIndex;
+    }
+
+    /**
+     * Preserve a copy of the current main expression at a new index.
+     * This assumes that initial evaluation completed suceessfully.
+     * @param in_history use a positive index so the result appears in the history.
+     * @return the new index
+     */
+    public long preserve(boolean in_history) {
+        ExprInfo ei = copy(MAIN_INDEX, true);
+        if (ei.mResultString == null) {
+            throw new AssertionError("Preserving unevaluated expression");
+        }
+        return addToDB(in_history, ei);
+    }
+
+    /**
+     * Preserve a copy of the current main expression as the most recent history entry,
+     * assuming it is already in teh database, but was lost from the cache.
+     */
+    public void represerve() {
+        // FIXME: Think about odd races in which other things happened before we get here.
+        ExprInfo ei = copy(MAIN_INDEX, true);
+        long resultIndex = getMaxIndex();
+        if (mExprs.get(resultIndex) != null) {
+            throw new AssertionError("result slot already occupied! + Slot = " + resultIndex);
+        }
+        mExprs.put(resultIndex, ei);
+    }
+    /**
+     * @return the {@link CalculatorExpr} representation of the result of the given
+     * expression.
+     * The resulting expression contains a single "token" with the pre-evaluated result.
+     * The client should ensure that this is never invoked unless initial evaluation of the
+     * expression has been completed.
+     */
+    private CalculatorExpr getCollapsedExpr(long index) {
+        long real_index = (index == MAIN_INDEX) ? preserve(false) : index;
+        final ExprInfo ei = mExprs.get(real_index);
+        final String rs = ei.mResultString;
+        final int dotIndex = rs.indexOf('.');
+        final int leastDigOffset = getLsdOffset(ei.mVal.get(), rs, dotIndex);
+        return ei.mExpr.abbreviate(real_index,
+                getShortString(rs, getMsdIndexOf(rs), leastDigOffset));
+    }
+
+    /**
+     * Abbreviate the indicated expression to a pre-evaluated expression node,
+     * and use that as the new main expression.
      * This should not be called unless the expression was previously evaluated and produced a
      * non-error result.  Pre-evaluated expressions can never represent an expression for which
      * evaluation to a constructive real diverges.  Subsequent re-evaluation will also not
      * diverge, though it may generate errors of various kinds.  E.g.  sqrt(-10^-1000) .
      */
-    public void collapse() {
-        final CalculatorExpr abbrvExpr = getResultExpr();
-        clearPreservingTimeout();
-        mExpr.append(abbrvExpr);
+    public void collapse(long index) {
+        final CalculatorExpr abbrvExpr = getCollapsedExpr(index);
+        clearMainPreservingTimeout();
+        mMainExpr.mExpr.append(abbrvExpr);
         mChangedValue = true;
         mHasTrigFuncs = false;  // Degree mode no longer affects expression value.
     }
@@ -1061,21 +1405,138 @@
         mChangedValue = true;
     }
 
+    private abstract class SetWhenDoneListener implements EvaluationListener {
+        private void badCall() {
+            throw new AssertionError("unexpected callback");
+        }
+        abstract void setNow();
+        @Override
+        public void onCancelled(long index) {}  // Extremely unlikely; leave unset.
+        @Override
+        public void onError(long index, int errorId) {}  // Extremely unlikely; leave unset.
+        @Override
+        public void onEvaluate(long index, int initPrecOffset, int msdIndex, int lsdOffset,
+                String truncatedWholePart) {
+            setNow();
+        }
+        @Override
+        public void onReevaluate(long index) {
+            badCall();
+        }
+    }
+
+    private class SetMemoryWhenDoneListener extends SetWhenDoneListener {
+        final long mIndex;
+        final boolean mPersist;
+        SetMemoryWhenDoneListener(long index, boolean persist) {
+            mIndex = index;
+            mPersist = persist;
+        }
+        @Override
+        void setNow() {
+            if (mMemoryIndex != 0) {
+                throw new AssertionError("Overwriting nonzero memory index");
+            }
+            if (mPersist) {
+                setMemoryIndex(mIndex);
+            } else {
+                mMemoryIndex = mIndex;
+            }
+        }
+    }
+
+    private class SetSavedWhenDoneListener extends SetWhenDoneListener {
+        final long mIndex;
+        SetSavedWhenDoneListener(long index) {
+            mIndex = index;
+        }
+        @Override
+        void setNow() {
+            mSavedIndex = mIndex;
+        }
+    }
+
     /**
-     * Abbreviate current expression, and put result in mSaved.
+     * Set the local and persistent memory index.
+     */
+    private void setMemoryIndex(long index) {
+        mMemoryIndex = index;
+        mSharedPrefs.edit()
+                .putLong(KEY_PREF_MEMORY_INDEX, index)
+                .apply();
+    }
+
+    /**
+     * Set the local and persistent saved index.
+     */
+    private void setSavedIndex(long index) {
+        mSavedIndex = index;
+        mSharedPrefs.edit()
+                .putLong(KEY_PREF_SAVED_INDEX, index)
+                .apply();
+    }
+
+    /**
+     * Set mMemoryIndex (possibly including the persistent version) to index when we finish
+     * evaluating the corresponding expression.
+     */
+    void setMemoryIndexWhenEvaluated(long index, boolean persist) {
+        requireResult(index, new SetMemoryWhenDoneListener(index, persist), mDummyCharMetricsInfo);
+    }
+
+    /**
+     * Set mSavedIndex (not the persistent version) to index when we finish evaluating
+     * the corresponding expression.
+     */
+    void setSavedIndexWhenEvaluated(long index) {
+        requireResult(index, new SetSavedWhenDoneListener(index), mDummyCharMetricsInfo);
+    }
+
+    /**
+     * Save an immutable version of the expression at the given index as the saved value.
      * mExpr is left alone.  Return false if result is unavailable.
      */
-    public boolean collapseToSaved() {
-        if (mResultString == null) {
+    private boolean copyToSaved(long index) {
+        if (mExprs.get(index).mResultString == null) {
             return false;
         }
-        final CalculatorExpr abbrvExpr = getResultExpr();
-        mSaved.clear();
-        mSaved.append(abbrvExpr);
-        mLongSavedTimeout = mLongTimeout;
+        setSavedIndex((index == MAIN_INDEX) ? preserve(false) : index);
         return true;
     }
 
+    /**
+     * Save an immutable version of the expression at the given index as the "memory" value.
+     * The expression at index is presumed to have been evaluated.
+     */
+    public void copyToMemory(long index) {
+        setMemoryIndex((index == MAIN_INDEX) ? preserve(false) : index);
+    }
+
+    /**
+     * Save an an expression representing the sum of "memory" and the expression with the
+     * given index. Make mMemoryIndex point to it when we complete evaluating.
+     */
+    public void addToMemory(long index) {
+        ExprInfo newEi = sum(index, mMemoryIndex);
+        long newIndex = addToDB(false, newEi);
+        mMemoryIndex = 0;  // Invalidate while we're evaluating.
+        setMemoryIndexWhenEvaluated(newIndex, true /* persist */);
+    }
+
+    /**
+     * Return index of "saved" expression, or 0.
+     */
+    public long getSavedIndex() {
+        return mSavedIndex;
+    }
+
+    /**
+     * Return index of "memory" expression, or 0.
+     */
+    public long getMemoryIndex() {
+        return mMemoryIndex;
+    }
+
     private Uri uriForSaved() {
         return new Uri.Builder().scheme("tag")
                                 .encodedOpaquePart(mSavedName)
@@ -1083,12 +1544,11 @@
     }
 
     /**
-     * Collapse the current expression to mSaved and return a URI describing it.
-     * describing this particular result, so that we can refer to it
-     * later.
+     * Save the index expression as the saved location and return a URI describing it.
+     * The URI is used to distinguish this particular result from others we may generate.
      */
-    public Uri capture() {
-        if (!collapseToSaved()) return null;
+    public Uri capture(long index) {
+        if (!copyToSaved(index)) return null;
         // Generate a new (entirely private) URI for this result.
         // Attempt to conform to RFC4151, though it's unclear it matters.
         final TimeZone tz = TimeZone.getDefault();
@@ -1097,21 +1557,27 @@
         final String isoDate = df.format(new Date());
         mSavedName = "calculator2.android.com," + isoDate + ":"
                 + (new Random().nextInt() & 0x3fffffff);
+        mSharedPrefs.edit()
+                .putString(KEY_PREF_SAVED_NAME, mSavedName)
+                .apply();
         return uriForSaved();
     }
 
     public boolean isLastSaved(Uri uri) {
-        return uri.equals(uriForSaved());
-    }
-
-    public void appendSaved() {
-        mChangedValue = true;
-        mLongTimeout |= mLongSavedTimeout;
-        mExpr.append(mSaved);
+        return mSavedIndex != 0 && uri.equals(uriForSaved());
     }
 
     /**
-     * Add the power of 10 operator to the expression.
+     * Append the expression at index as a pre-evaluated expression to the main expression.
+     */
+    public void appendExpr(long index) {
+        mChangedValue = true;
+        mMainExpr.mLongTimeout |= mExprs.get(index).mLongTimeout;
+        mMainExpr.mExpr.append(getCollapsedExpr(index));
+    }
+
+    /**
+     * Add the power of 10 operator to the main expression.
      * This is treated essentially as a macro expansion.
      */
     private void add10pow() {
@@ -1119,21 +1585,74 @@
         ten.add(R.id.digit_1);
         ten.add(R.id.digit_0);
         mChangedValue = true;  // For consistency.  Reevaluation is probably not useful.
-        mExpr.append(ten);
-        mExpr.add(R.id.op_pow);
+        mMainExpr.mExpr.append(ten);
+        mMainExpr.mExpr.add(R.id.op_pow);
     }
 
     /**
-     * Retrieve the main expression being edited.
-     * It is the callee's reponsibility to call cancelAll to cancel ongoing concurrent
-     * computations before modifying the result.  The resulting expression should only
-     * be modified by the caller if either the expression value doesn't change, or in
-     * combination with another add() or delete() call that makes the value change apparent
-     * to us.
-     * TODO: Perhaps add functionality so we can keep this private?
+     * Ensure that the expression with the given index is in mExprs.
+     * We assume that if it's either already in mExprs or mExprDB.
+     * When we're done, the expression in mExprs may still contain references to other
+     * subexpressions that are not yet cached.
      */
-    public CalculatorExpr getExpr() {
-        return mExpr;
+    private ExprInfo ensureExprIsCached(long index) {
+        ExprInfo ei = mExprs.get(index);
+        if (ei != null) {
+            return ei;
+        }
+        ExpressionDB.RowData row = mExprDB.getRow(index);
+        DataInputStream serializedExpr =
+                new DataInputStream(new ByteArrayInputStream(row.mExpression));
+        try {
+            ei = new ExprInfo(new CalculatorExpr(serializedExpr), row.degreeMode());
+            ei.mTimeStamp = row.mTimeStamp;
+            ei.mUtcOffset = row.utcOffset();
+        } catch(IOException e) {
+            throw new AssertionError("IO Exception without real IO:" + e);
+        }
+        ExprInfo newEi = mExprs.putIfAbsent(index, ei);
+        return newEi == null ? ei : newEi;
+    }
+
+    @Override
+    public CalculatorExpr getExpr(long index) {
+        return ensureExprIsCached(index).mExpr;
+    }
+
+    /*
+     * Return timestamp associated with the expression in milliseconds since epoch.
+     * Yields zero if the expression has not been written to or read from the database.
+     */
+    public long getTimeStamp(long index) {
+        return ensureExprIsCached(index).mTimeStamp;
+    }
+
+    /*
+     * Return UTC offset associated with the expression in milliseconds.
+     */
+    public long getUtcOffset(long index) {
+        return ensureExprIsCached(index).mUtcOffset;
+    }
+
+    @Override
+    public boolean getDegreeMode(long index) {
+        return ensureExprIsCached(index).mDegreeMode;
+    }
+
+    @Override
+    public UnifiedReal getResult(long index) {
+        return ensureExprIsCached(index).mVal.get();
+    }
+
+    @Override
+    public UnifiedReal putResultIfAbsent(long index, UnifiedReal result) {
+        ExprInfo ei = mExprs.get(index);
+        if (ei.mVal.compareAndSet(null, result)) {
+            return result;
+        } else {
+            // Cannot change once non-null.
+            return ei.mVal.get();
+        }
     }
 
     /**
@@ -1197,7 +1716,35 @@
         for (; i < end; ++i) {
             exp = 10 * exp + Character.digit(s.charAt(i), 10);
         }
-        mExpr.addExponent(sign * exp);
+        mMainExpr.mExpr.addExponent(sign * exp);
         mChangedValue = true;
     }
+
+    /**
+     * Generate a String representation of the expression at the given index.
+     * This has the side effect of adding the expression to mExprs.
+     * The expression must exist in the database.
+     */
+    public String getExprAsString(long index) {
+        return getExpr(index).toSpannableStringBuilder(mActivity).toString();
+    }
+
+    /**
+     * Generate a String representation of all expressions in the database.
+     * Debugging only.
+     */
+    public String historyAsString() {
+        final long startIndex = getMinIndex();
+        final long endIndex = getMaxIndex();
+        final StringBuilder sb = new StringBuilder();
+        for (long i = getMinIndex(); i < ExpressionDB.MAXIMUM_MIN_INDEX; ++i) {
+            sb.append(i).append(": ").append(getExprAsString(i)).append("\n");
+        }
+        for (long i = 1; i < getMaxIndex(); ++i) {
+            sb.append(i).append(": ").append(getExprAsString(i)).append("\n");
+        }
+        sb.append("Memory index = ").append(getMemoryIndex());
+        sb.append(" Saved index = ").append(getSavedIndex()).append("\n");
+        return sb.toString();
+    }
 }
diff --git a/src/com/android/calculator2/ExpressionDB.java b/src/com/android/calculator2/ExpressionDB.java
new file mode 100644
index 0000000..0e3a9fa
--- /dev/null
+++ b/src/com/android/calculator2/ExpressionDB.java
@@ -0,0 +1,394 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+// FIXME: We need to rethink the error handling here. Do we want to revert to history-less
+// operation if something goes wrong with the database?
+// TODO: This tries to ensure strong thread-safety, i.e. each call looks atomic, both to
+// other threads and other database users. Is this useful?
+// TODO: Especially if we notice serious performance issues on rotation in the history
+// view, we may need to use a CursorLoader or some other scheme to preserve the database
+// across rotations.
+
+package com.android.calculator2;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.os.AsyncTask;
+import android.provider.BaseColumns;
+import android.util.Log;
+
+import java.util.Calendar;
+import java.util.TimeZone;
+
+public class ExpressionDB {
+    /* Table contents */
+    public static class ExpressionEntry implements BaseColumns {
+        public static final String TABLE_NAME = "expressions";
+        public static final String COLUMN_NAME_EXPRESSION = "expression";
+        public static final String COLUMN_NAME_FLAGS = "flags";
+        // Time stamp as returned by currentTimeMillis().
+        public static final String COLUMN_NAME_TIMESTAMP = "timeStamp";
+        // UTC offset at the locale when the expression was saved. In multiples of 15 minutes. 
+        public static final String COLUMN_NAME_COMPRESSED_UTC_OFFSET = "compressedUtcOffset";
+    }
+
+    /* Data to be written to or read from a row in the table */
+    public static class RowData {
+        private static final int DEGREE_MODE = 2;
+        private static final int LONG_TIMEOUT = 1;
+        public final byte[] mExpression;
+        public final int mFlags;
+        public long mTimeStamp;  // 0 ==> this and next field to be filled in when written.
+        public byte mCompressedUtcOffset;  // multiples of 15 minutes.
+        private static int flagsFromDegreeAndTimeout(Boolean DegreeMode, Boolean LongTimeout) {
+            return (DegreeMode ? DEGREE_MODE : 0) | (LongTimeout ? LONG_TIMEOUT : 0);
+        }
+        private boolean degreeModeFromFlags(int flags) {
+            return (flags & DEGREE_MODE) != 0;
+        }
+        private boolean longTimeoutFromFlags(int flags) {
+            return (flags & LONG_TIMEOUT) != 0;
+        }
+        private static final int MILLIS_IN_15_MINS = 15 * 60 * 1000;
+        private static byte compressUtcOffset(int utcOffsetMillis) {
+            // Rounded division, though it shouldn't really matter.
+            if (utcOffsetMillis > 0) {
+                return (byte) ((utcOffsetMillis + MILLIS_IN_15_MINS / 2) / MILLIS_IN_15_MINS);
+            } else {
+                return (byte) ((utcOffsetMillis - MILLIS_IN_15_MINS / 2) / MILLIS_IN_15_MINS);
+            }
+        }
+        private static int uncompressUtcOffset(byte compressedUtcOffset) {
+            return MILLIS_IN_15_MINS * (int)compressedUtcOffset;
+        }
+        private RowData(byte[] expr, int flags, long timeStamp, byte compressedUtcOffset) {
+            mExpression = expr;
+            mFlags = flags;
+            mTimeStamp = timeStamp;
+            mCompressedUtcOffset = compressedUtcOffset;
+        }
+        /**
+         * More client-friendly constructor that hides implementation ugliness.
+         * utcOffset here is uncompressed, in milliseconds.
+         * A zero timestamp will cause it to be automatically filled in.
+         */
+        public RowData(byte[] expr, boolean degreeMode, boolean longTimeout, long timeStamp,
+                int utcOffset) {
+            this(expr, flagsFromDegreeAndTimeout(degreeMode, longTimeout), timeStamp,
+                    compressUtcOffset(utcOffset));
+        }
+        public boolean degreeMode() {
+            return degreeModeFromFlags(mFlags);
+        }
+        public boolean longTimeout() {
+            return longTimeoutFromFlags(mFlags);
+        }
+        /**
+         * Return UTC offset for timestamp in milliseconds.
+         */
+        public int utcOffset() {
+            return uncompressUtcOffset(mCompressedUtcOffset);
+        }
+        /**
+         * Return a ContentValues object representing the current data.
+         */
+        public ContentValues toContentValues() {
+            ContentValues cvs = new ContentValues();
+            cvs.put(ExpressionEntry.COLUMN_NAME_EXPRESSION, mExpression);
+            cvs.put(ExpressionEntry.COLUMN_NAME_FLAGS, mFlags);
+            if (mTimeStamp == 0) {
+                mTimeStamp = System.currentTimeMillis();
+                final TimeZone timeZone = Calendar.getInstance().getTimeZone();
+                mCompressedUtcOffset = compressUtcOffset(timeZone.getOffset(mTimeStamp));
+            }
+            cvs.put(ExpressionEntry.COLUMN_NAME_TIMESTAMP, mTimeStamp);
+            cvs.put(ExpressionEntry.COLUMN_NAME_COMPRESSED_UTC_OFFSET, mCompressedUtcOffset);
+            return cvs;
+        }
+    }
+
+    private static final String SQL_CREATE_ENTRIES =
+            "CREATE TABLE " + ExpressionEntry.TABLE_NAME + " (" +
+            ExpressionEntry._ID + " INTEGER PRIMARY KEY," +
+            ExpressionEntry.COLUMN_NAME_EXPRESSION + " BLOB," +
+            ExpressionEntry.COLUMN_NAME_FLAGS + " INTEGER," +
+            ExpressionEntry.COLUMN_NAME_TIMESTAMP + " INTEGER," +
+            ExpressionEntry.COLUMN_NAME_COMPRESSED_UTC_OFFSET + " INTEGER)";
+    private static final String SQL_DELETE_ENTRIES =
+            "DROP TABLE IF EXISTS " + ExpressionEntry.TABLE_NAME;
+    private static final String SQL_GET_MIN = "SELECT MIN(" + ExpressionEntry._ID +
+            ") FROM " + ExpressionEntry.TABLE_NAME;
+    private static final String SQL_GET_MAX = "SELECT MAX(" + ExpressionEntry._ID +
+            ") FROM " + ExpressionEntry.TABLE_NAME;
+    private static final String SQL_GET_ROW = "SELECT * FROM " + ExpressionEntry.TABLE_NAME +
+            " WHERE " + ExpressionEntry._ID + " = ?";
+
+    private class ExpressionDBHelper extends SQLiteOpenHelper {
+        // If you change the database schema, you must increment the database version.
+        public static final int DATABASE_VERSION = 1;
+        public static final String DATABASE_NAME = "Expressions.db";
+
+        public ExpressionDBHelper(Context context) {
+            super(context, DATABASE_NAME, null, DATABASE_VERSION);
+        }
+        public void onCreate(SQLiteDatabase db) {
+            db.execSQL(SQL_CREATE_ENTRIES);
+        }
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            // For now just throw away history on database version upgrade/downgrade.
+            db.execSQL(SQL_DELETE_ENTRIES);
+            onCreate(db);
+        }
+        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            onUpgrade(db, oldVersion, newVersion);
+        }
+    }
+
+    private ExpressionDBHelper mExpressionDBHelper;
+
+    private SQLiteDatabase mExpressionDB;  // Constant after initialization.
+
+    private boolean mBadDB = false;      // Database initialization failed.
+
+    // Never allocate new negative indicees (row ids) >= MAXIMUM_MIN_INDEX.
+    public static final long MAXIMUM_MIN_INDEX = -10;
+
+    // Minimum index value in DB.
+    private long mMinIndex;
+    // Maximum index value in DB.
+    private long mMaxIndex;
+    // mMinIndex and mMaxIndex are correct.
+    private boolean mMinMaxValid;
+
+    // mLock protects mExpressionDB and mBadDB, though we access mExpressionDB without
+    // synchronization after it's known to be initialized.  Used to wait for database
+    // initialization. Also protects mMinIndex, mMaxIndex, and mMinMaxValid.
+    private Object mLock = new Object();
+
+    public ExpressionDB(Context context) {
+        mExpressionDBHelper = new ExpressionDBHelper(context);
+        AsyncInitializer initializer = new AsyncInitializer();
+        initializer.execute(mExpressionDBHelper);
+    }
+
+    private boolean getBadDB() {
+        synchronized(mLock) {
+            return mBadDB;
+        }
+    }
+
+    private void setBadDB() {
+        synchronized(mLock) {
+            mBadDB = true;
+        }
+    }
+
+    /**
+     * Set mExpressionDB and compute minimum and maximum indices in the background.
+     */
+    private class AsyncInitializer extends AsyncTask<ExpressionDBHelper, Void, SQLiteDatabase> {
+        @Override
+        protected SQLiteDatabase doInBackground(ExpressionDBHelper... helper) {
+            SQLiteDatabase result;
+            try {
+                result = helper[0].getWritableDatabase();
+                // We notify here, since there are unlikely cases in which the UI thread
+                // may be blocked on us, preventing onPostExecute from running.
+                synchronized(mLock) {
+                    mExpressionDB = result;
+                    mLock.notifyAll();
+                }
+                long min, max;
+                try (Cursor minResult = result.rawQuery(SQL_GET_MIN, null)) {
+                    if (!minResult.moveToFirst()) {
+                        // Empty database.
+                        min = MAXIMUM_MIN_INDEX;
+                    } else {
+                        min = Math.min(minResult.getLong(0), MAXIMUM_MIN_INDEX);
+                    }
+                }
+                try (Cursor maxResult = result.rawQuery(SQL_GET_MAX, null)) {
+                    if (!maxResult.moveToFirst()) {
+                        // Empty database.
+                        max = 0L;
+                    } else {
+                        max = Math.max(maxResult.getLong(0), 0L);
+                    }
+                }
+                synchronized(mLock) {
+                    mMinIndex = min;
+                    mMaxIndex = max;
+                    mMinMaxValid = true;
+                    mLock.notifyAll();
+                }
+            } catch(SQLiteException e) {
+                Log.e("Calculator", "Database initialization failed.\n", e);
+                synchronized(mLock) {
+                    mBadDB = true;
+                    mLock.notifyAll();
+                }
+                return null;
+            }
+            return result;
+        }
+
+        @Override
+        protected void onPostExecute(SQLiteDatabase result) {
+            if (result == null) {
+                throw new AssertionError("Failed to open history DB");
+                // TODO: Should we try to run without persistent history instead?
+            } // else doInBackground already set expressionDB.
+        }
+        // On cancellation we do nothing;
+    }
+
+    /**
+     * Wait until expression DB is ready.
+     * This should usually be a no-op, since we set up the DB on creation. But there are a few
+     * cases, such as restarting the calculator in history mode, when we currently can't do
+     * anything but wait, possibly even in the UI thread.
+     */
+    private void waitForExpressionDB() {
+        synchronized(mLock) {
+            while (mExpressionDB == null && !mBadDB) {
+                try {
+                    mLock.wait();
+                } catch(InterruptedException e) {
+                    mBadDB = true;
+                }
+            }
+            if (mBadDB) {
+                throw new AssertionError("Failed to open history DB");
+            }
+        }
+    }
+
+    /**
+     * Wait until the minimum key has been computed.
+     */
+    private void waitForMinMaxValid() {
+        synchronized(mLock) {
+            while (!mMinMaxValid && !mBadDB) {
+                try {
+                    mLock.wait();
+                } catch(InterruptedException e) {
+                    mBadDB = true;
+                }
+            }
+            if (mBadDB) {
+                throw new AssertionError("Failed to compute minimum key");
+            }
+        }
+    }
+
+    public synchronized void eraseAll() {
+        waitForExpressionDB();
+        mExpressionDB.execSQL("SQL_DELETE_ENTRIES");
+        try {
+            mExpressionDB.execSQL("VACUUM");
+        } catch(Exception e) {
+            Log.v("Calculator", "Database VACUUM failed\n", e);
+            // FIXME: probably needed only if there is danger of concurrent execution.
+        }
+    }
+
+    /**
+     * Update a row in place.
+     * Currently unused, since only the main expression is updated in place, and we
+     * don't save that in the DB.
+     */
+    public void updateRow(long index, RowData data) {
+        if (index == -1) {
+            setBadDB();
+            throw new AssertionError("Can't insert row index of -1");
+        }
+        ContentValues cvs = data.toContentValues();
+        cvs.put(ExpressionEntry._ID, index);
+        waitForExpressionDB();
+        long result = mExpressionDB.replace(ExpressionEntry.TABLE_NAME, null, cvs);
+        if (result == -1) {
+            throw new AssertionError("Row update failed");
+        }
+    }
+
+    /**
+     * Add a row with index outside existing range.
+     * The returned index will be larger than any existing index unless negative_index is true.
+     * In that case it will be smaller than any existing index and smaller than MAXIMUM_MIN_INDEX.
+     */
+    public long addRow(boolean negative_index, RowData data) {
+        long result;
+        long newIndex;
+        waitForMinMaxValid();
+        synchronized(mLock) {
+            if (negative_index) {
+                newIndex = mMinIndex - 1;
+                mMinIndex = newIndex;
+            } else {
+                newIndex = mMaxIndex + 1;
+                mMaxIndex = newIndex;
+            }
+            ContentValues cvs = data.toContentValues();
+            cvs.put(ExpressionEntry._ID, newIndex);
+            result = mExpressionDB.insert(ExpressionEntry.TABLE_NAME, null, cvs);
+        }
+        if (result != newIndex) {
+            throw new AssertionError("Expected row id " + newIndex + ", got " + result);
+        }
+        return result;
+    }
+
+    /**
+     * Retrieve the row with the given index.
+     * Such a row must exist.
+     */
+    public RowData getRow(long index) {
+        RowData result;
+        waitForExpressionDB();
+        String args[] = new String[] { Long.toString(index) };
+        Cursor resultC = mExpressionDB.rawQuery(SQL_GET_ROW, args);
+        if (!resultC.moveToFirst()) {
+            throw new AssertionError("Missing Row");
+        } else {
+            result = new RowData(resultC.getBlob(1), resultC.getInt(2) /* flags */,
+                    resultC.getLong(3) /* timestamp */, (byte)resultC.getInt(4) /* UTC offset */);
+        }
+        return result;
+    }
+
+    public long getMinIndex() {
+        waitForMinMaxValid();
+        synchronized(mLock) {
+            return mMinIndex;
+        }
+    }
+
+    public long getMaxIndex() {
+        waitForMinMaxValid();
+        synchronized(mLock) {
+            return mMaxIndex;
+        }
+    }
+
+    public void close() {
+        mExpressionDBHelper.close();
+    }
+
+}
diff --git a/src/com/android/calculator2/KeyMaps.java b/src/com/android/calculator2/KeyMaps.java
index e82f35d..c78cf2e 100644
--- a/src/com/android/calculator2/KeyMaps.java
+++ b/src/com/android/calculator2/KeyMaps.java
@@ -82,11 +82,11 @@
                 return context.getString(R.string.op_div);
             case R.id.op_add:
                 return context.getString(R.string.op_add);
+            case R.id.op_sub:
+                return context.getString(R.string.op_sub);
             case R.id.op_sqr:
                 // Button label doesn't work.
                 return context.getString(R.string.squared);
-            case R.id.op_sub:
-                return context.getString(R.string.op_sub);
             case R.id.dec_point:
                 return context.getString(R.string.dec_point);
             case R.id.digit_0:
@@ -115,6 +115,142 @@
     }
 
     /**
+     * Map key id to a single byte, somewhat human readable, description.
+     * Used to serialize expressions in the database.
+     * The result is in the range 0x20-0x7f.
+     */
+    public static byte toByte(int id) {
+        char result;
+        // We only use characters with single-byte UTF8 encodings in the range 0x20-0x7F.
+        switch(id) {
+            case R.id.const_pi:
+                result = 'p';
+                break;
+            case R.id.const_e:
+                result = 'e';
+                break;
+            case R.id.op_sqrt:
+                result = 'r';
+                break;
+            case R.id.op_fact:
+                result = '!';
+                break;
+            case R.id.op_pct:
+                result = '%';
+                break;
+            case R.id.fun_sin:
+                result = 's';
+                break;
+            case R.id.fun_cos:
+                result = 'c';
+                break;
+            case R.id.fun_tan:
+                result = 't';
+                break;
+            case R.id.fun_arcsin:
+                result = 'S';
+                break;
+            case R.id.fun_arccos:
+                result = 'C';
+                break;
+            case R.id.fun_arctan:
+                result = 'T';
+                break;
+            case R.id.fun_ln:
+                result = 'l';
+                break;
+            case R.id.fun_log:
+                result = 'L';
+                break;
+            case R.id.fun_exp:
+                result = 'E';
+                break;
+            case R.id.lparen:
+                result = '(';
+                break;
+            case R.id.rparen:
+                result = ')';
+                break;
+            case R.id.op_pow:
+                result = '^';
+                break;
+            case R.id.op_mul:
+                result = '*';
+                break;
+            case R.id.op_div:
+                result = '/';
+                break;
+            case R.id.op_add:
+                result = '+';
+                break;
+            case R.id.op_sub:
+                result = '-';
+                break;
+            case R.id.op_sqr:
+                result = '2';
+                break;
+            default:
+                throw new AssertionError("Unexpected key id");
+        }
+        return (byte)result;
+    }
+
+    /**
+     * Map single byte encoding generated by key id generated by toByte back to
+     * key id.
+     */
+    public static int fromByte(byte b) {
+        switch((char)b) {
+            case 'p':
+                return R.id.const_pi;
+            case 'e':
+                return R.id.const_e;
+            case 'r':
+                return R.id.op_sqrt;
+            case '!':
+                return R.id.op_fact;
+            case '%':
+                return R.id.op_pct;
+            case 's':
+                return R.id.fun_sin;
+            case 'c':
+                return R.id.fun_cos;
+            case 't':
+                return R.id.fun_tan;
+            case 'S':
+                return R.id.fun_arcsin;
+            case 'C':
+                return R.id.fun_arccos;
+            case 'T':
+                return R.id.fun_arctan;
+            case 'l':
+                return R.id.fun_ln;
+            case 'L':
+                return R.id.fun_log;
+            case 'E':
+                return R.id.fun_exp;
+            case '(':
+                return R.id.lparen;
+            case ')':
+                return R.id.rparen;
+            case '^':
+                return R.id.op_pow;
+            case '*':
+                return R.id.op_mul;
+            case '/':
+                return R.id.op_div;
+            case '+':
+                return R.id.op_add;
+            case '-':
+                return R.id.op_sub;
+            case '2':
+                return R.id.op_sqr;
+            default:
+                throw new AssertionError("Unexpected single byte operator encoding");
+        }
+    }
+
+    /**
      * Map key id to corresponding (internationalized) descriptive string that can be used
      * to correctly read back a formula.
      * Only used for operators and individual characters; not used inside constants.