am dc869669: am 2be4fdbf: Preserve/restore calculator instance state

* commit 'dc869669713dfcc6369b8fe44817157c11afcca9':
  Preserve/restore calculator instance state
diff --git a/src/com/android/calculator2/Calculator.java b/src/com/android/calculator2/Calculator.java
index 256cf4e..ac8c86a 100644
--- a/src/com/android/calculator2/Calculator.java
+++ b/src/com/android/calculator2/Calculator.java
@@ -31,7 +31,6 @@
 import android.text.Editable;
 import android.text.TextUtils;
 import android.text.TextWatcher;
-import android.view.KeyEvent;
 import android.view.View;
 import android.view.View.OnLongClickListener;
 import android.view.ViewAnimationUtils;
@@ -46,8 +45,16 @@
 public class Calculator extends Activity
         implements OnTextSizeChangeListener, EvaluateCallback, OnLongClickListener {
 
-    public static final String CALCULATOR_ACTIVITY_CURRENT_STATE =
-            Calculator.class.getSimpleName() + "_currentState";
+    private static final String NAME = Calculator.class.getName();
+
+    // instance state keys
+    private static final String KEY_CURRENT_STATE = NAME + "_currentState";
+    private static final String KEY_CURRENT_EXPRESSION = NAME + "_currentExpression";
+
+    /**
+     * Constant for an invalid resource id.
+     */
+    public static final int INVALID_RES_ID = -1;
 
     private enum CalculatorState {
         INPUT, EVALUATE, RESULT, ERROR
@@ -72,19 +79,19 @@
     private final Editable.Factory mFormulaEditableFactory = new Editable.Factory() {
         @Override
         public Editable newEditable(CharSequence source) {
-            return new CalculatorExpressionBuilder(Calculator.this, source,
-                    mCurrentState == CalculatorState.INPUT);
+            final boolean isEdited = mCurrentState == CalculatorState.INPUT
+                    || mCurrentState == CalculatorState.ERROR;
+            return new CalculatorExpressionBuilder(source, mTokenizer, isEdited);
         }
     };
 
     private CalculatorState mCurrentState;
+    private CalculatorExpressionTokenizer mTokenizer;
     private CalculatorExpressionEvaluator mEvaluator;
 
     private CalculatorEditText mFormulaEditText;
     private CalculatorEditText mResultEditText;
-
     private ViewPager mPadViewPager;
-
     private View mDeleteButton;
     private View mClearButton;
 
@@ -98,28 +105,32 @@
 
         mFormulaEditText = (CalculatorEditText) findViewById(R.id.formula);
         mResultEditText = (CalculatorEditText) findViewById(R.id.result);
-
         mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
-
         mDeleteButton = findViewById(R.id.del);
         mClearButton = findViewById(R.id.clr);
 
+        mTokenizer = new CalculatorExpressionTokenizer(this);
+        mEvaluator = new CalculatorExpressionEvaluator(mTokenizer);
+
         savedInstanceState = savedInstanceState == null ? Bundle.EMPTY : savedInstanceState;
-        setState(CalculatorState.values()[savedInstanceState.getInt(
-                CALCULATOR_ACTIVITY_CURRENT_STATE, CalculatorState.INPUT.ordinal())]);
-        mEvaluator = new CalculatorExpressionEvaluator(this);
+        setState(CalculatorState.values()[
+                savedInstanceState.getInt(KEY_CURRENT_STATE, CalculatorState.INPUT.ordinal())]);
+        mFormulaEditText.setText(mTokenizer.getLocalizedExpression(
+                savedInstanceState.getString(KEY_CURRENT_EXPRESSION, "")));
+        mEvaluator.evaluate(mFormulaEditText.getText(), this);
 
         mFormulaEditText.setEditableFactory(mFormulaEditableFactory);
         mFormulaEditText.addTextChangedListener(mFormulaTextWatcher);
         mFormulaEditText.setOnTextSizeChangeListener(this);
-
         mDeleteButton.setOnLongClickListener(this);
     }
 
     @Override
     protected void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
-        outState.putInt(CALCULATOR_ACTIVITY_CURRENT_STATE, mCurrentState.ordinal());
+        outState.putInt(KEY_CURRENT_STATE, mCurrentState.ordinal());
+        outState.putString(KEY_CURRENT_EXPRESSION,
+                mTokenizer.getNormalizedExpression(mFormulaEditText.getText().toString()));
     }
 
     private void setState(CalculatorState state) {
@@ -184,8 +195,12 @@
                 }
                 break;
             case R.id.del:
-                mFormulaEditText.dispatchKeyEvent(
-                        new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
+                // Delete works like backspace; remove the last character from the expression.
+                final Editable formulaText = mFormulaEditText.getEditableText();
+                final int formulaLength = formulaText.length();
+                if (formulaLength > 0) {
+                    formulaText.delete(formulaLength - 1, formulaLength);
+                }
                 break;
             case R.id.clr:
                 onClear();
@@ -195,7 +210,7 @@
             case R.id.fun_log:
             case R.id.fun_sin:
             case R.id.fun_tan:
-                // add left paren after functions
+                // Add left parenthesis after functions.
                 mFormulaEditText.append(((Button) view).getText() + "(");
                 break;
             default:
@@ -216,11 +231,11 @@
     }
 
     @Override
-    public void onEvaluate(String expr, String result, String error) {
+    public void onEvaluate(String expr, String result, int errorResourceId) {
         if (mCurrentState == CalculatorState.INPUT) {
             mResultEditText.setText(result);
-        } else if (!TextUtils.isEmpty(error)) {
-            onError(error);
+        } else if (errorResourceId != INVALID_RES_ID) {
+            onError(errorResourceId);
         } else if (!TextUtils.isEmpty(result)) {
             onResult(result);
         } else if (mCurrentState == CalculatorState.EVALUATE) {
@@ -327,12 +342,18 @@
         });
     }
 
-    private void onError(final String error) {
+    private void onError(final int errorResourceId) {
+        if (mCurrentState != CalculatorState.EVALUATE) {
+            // Only animate error on evaluate.
+            mResultEditText.setText(errorResourceId);
+            return;
+        }
+
         reveal(mCurrentButton, R.color.calculator_error_color, new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
                 setState(CalculatorState.ERROR);
-                mResultEditText.setText(error);
+                mResultEditText.setText(errorResourceId);
             }
         });
     }
diff --git a/src/com/android/calculator2/CalculatorEditText.java b/src/com/android/calculator2/CalculatorEditText.java
index 63a5ae5..b5ab026 100644
--- a/src/com/android/calculator2/CalculatorEditText.java
+++ b/src/com/android/calculator2/CalculatorEditText.java
@@ -60,6 +60,10 @@
     private final float mMinimumTextSize;
     private final float mStepTextSize;
 
+    // Temporary objects for use in layout methods.
+    private final Paint mTempPaint = new TextPaint();
+    private final Rect mTempRect = new Rect();
+
     private int mWidthConstraint = -1;
     private OnTextSizeChangeListener mOnTextSizeChangeListener;
 
@@ -92,17 +96,6 @@
     }
 
     @Override
-    protected void onSelectionChanged(int selStart, int selEnd) {
-        final int textLength = getText() == null ? 0 : getText().length();
-        if (selStart != textLength || selEnd != textLength) {
-            // Pin the selection to the end of the current text.
-            setSelection(textLength);
-        }
-
-        super.onSelectionChanged(selStart, selEnd);
-    }
-
-    @Override
     public boolean onTouchEvent(MotionEvent event) {
         if (event.getActionMasked() == MotionEvent.ACTION_UP) {
             // Hack to prevent keyboard and insertion handle from showing.
@@ -123,8 +116,6 @@
     @Override
     protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
         super.onTextChanged(text, start, lengthBefore, lengthAfter);
-
-        setSelection(text.length());
         setTextSize(TypedValue.COMPLEX_UNIT_PX, getVariableTextSize(text.toString()));
     }
 
@@ -148,12 +139,15 @@
             return getTextSize();
         }
 
-        final Paint paint = new TextPaint(getPaint());
+        // Capture current paint state.
+        mTempPaint.set(getPaint());
+
+        // Step through increasing text sizes until the text would no longer fit.
         float lastFitTextSize = mMinimumTextSize;
         while (lastFitTextSize < mMaximumTextSize) {
             final float nextSize = Math.min(lastFitTextSize + mStepTextSize, mMaximumTextSize);
-            paint.setTextSize(nextSize);
-            if (paint.measureText(text) > mWidthConstraint) {
+            mTempPaint.setTextSize(nextSize);
+            if (mTempPaint.measureText(text) > mWidthConstraint) {
                 break;
             } else {
                 lastFitTextSize = nextSize;
@@ -167,12 +161,10 @@
     public int getCompoundPaddingTop() {
         // Measure the top padding from the capital letter height of the text instead of the top,
         // but don't remove more than the available top padding otherwise clipping may occur.
-        final Rect capBounds = new Rect();
-        getPaint().getTextBounds("H", 0, 1, capBounds);
+        getPaint().getTextBounds("H", 0, 1, mTempRect);
 
         final FontMetricsInt fontMetrics = getPaint().getFontMetricsInt();
-        final int paddingOffset = -(fontMetrics.ascent + capBounds.height());
-
+        final int paddingOffset = -(fontMetrics.ascent + mTempRect.height());
         return super.getCompoundPaddingTop() - Math.min(getPaddingTop(), paddingOffset);
     }
 
diff --git a/src/com/android/calculator2/CalculatorExpressionBuilder.java b/src/com/android/calculator2/CalculatorExpressionBuilder.java
index 028a504..7fb47be 100644
--- a/src/com/android/calculator2/CalculatorExpressionBuilder.java
+++ b/src/com/android/calculator2/CalculatorExpressionBuilder.java
@@ -25,10 +25,11 @@
     private final CalculatorExpressionTokenizer mTokenizer;
     private boolean mIsEdited;
 
-    public CalculatorExpressionBuilder(Context context, CharSequence text, boolean isEdited) {
+    public CalculatorExpressionBuilder(
+            CharSequence text, CalculatorExpressionTokenizer tokenizer, boolean isEdited) {
         super(text);
 
-        mTokenizer = CalculatorExpressionTokenizer.getInstance(context);
+        mTokenizer = tokenizer;
         mIsEdited = isEdited;
     }
 
diff --git a/src/com/android/calculator2/CalculatorExpressionEvaluator.java b/src/com/android/calculator2/CalculatorExpressionEvaluator.java
index 5a032c3..26cd404 100644
--- a/src/com/android/calculator2/CalculatorExpressionEvaluator.java
+++ b/src/com/android/calculator2/CalculatorExpressionEvaluator.java
@@ -16,8 +16,6 @@
 
 package com.android.calculator2;
 
-import android.content.Context;
-
 import org.javia.arity.Symbols;
 import org.javia.arity.SyntaxException;
 import org.javia.arity.Util;
@@ -30,15 +28,9 @@
     private final Symbols mSymbols;
     private final CalculatorExpressionTokenizer mTokenizer;
 
-    private final String mErrorNaN;
-    private final String mErrorSyntax;
-
-    public CalculatorExpressionEvaluator(Context context) {
+    public CalculatorExpressionEvaluator(CalculatorExpressionTokenizer tokenizer) {
         mSymbols = new Symbols();
-        mTokenizer = CalculatorExpressionTokenizer.getInstance(context);
-
-        mErrorNaN = context.getString(R.string.error_nan);
-        mErrorSyntax = context.getString(R.string.error_syntax);
+        mTokenizer = tokenizer;
     }
 
     public void evaluate(CharSequence expr, EvaluateCallback callback) {
@@ -54,8 +46,8 @@
         }
 
         try {
-            if (expr == null || expr.length() == 0 || Double.valueOf(expr) != null) {
-                callback.onEvaluate(expr, null, null);
+            if (expr.length() == 0 || Double.valueOf(expr) != null) {
+                callback.onEvaluate(expr, null, Calculator.INVALID_RES_ID);
                 return;
             }
         } catch (NumberFormatException e) {
@@ -65,20 +57,21 @@
         try {
             double result = mSymbols.eval(expr);
             if (Double.isNaN(result)) {
-                callback.onEvaluate(expr, null, mErrorNaN);
+                callback.onEvaluate(expr, null, R.string.error_nan);
             } else {
                 // The arity library uses floating point arithmetic when evaluating the expression
                 // leading to precision errors in the result. The method doubleToString hides these
                 // errors; rounding the result by dropping N digits of precision.
-                callback.onEvaluate(expr, mTokenizer.getLocalizedExpression(
-                        Util.doubleToString(result, MAX_DIGITS, ROUNDING_DIGITS)), null);
+                final String resultString = mTokenizer.getLocalizedExpression(
+                        Util.doubleToString(result, MAX_DIGITS, ROUNDING_DIGITS));
+                callback.onEvaluate(expr, resultString, Calculator.INVALID_RES_ID);
             }
         } catch (SyntaxException e) {
-            callback.onEvaluate(expr, null, mErrorSyntax);
+            callback.onEvaluate(expr, null, R.string.error_syntax);
         }
     }
 
     public interface EvaluateCallback {
-        public void onEvaluate(String expr, String result, String error);
+        public void onEvaluate(String expr, String result, int errorResourceId);
     }
 }
diff --git a/src/com/android/calculator2/CalculatorExpressionTokenizer.java b/src/com/android/calculator2/CalculatorExpressionTokenizer.java
index c4e2ab4..b9c91e2 100644
--- a/src/com/android/calculator2/CalculatorExpressionTokenizer.java
+++ b/src/com/android/calculator2/CalculatorExpressionTokenizer.java
@@ -24,19 +24,9 @@
 
 public class CalculatorExpressionTokenizer {
 
-    private static CalculatorExpressionTokenizer sSharedInstance;
-
-    public static CalculatorExpressionTokenizer getInstance(Context context) {
-        if (sSharedInstance == null) {
-            sSharedInstance = new CalculatorExpressionTokenizer(context);
-        }
-
-        return sSharedInstance;
-    }
-
     private final Map<String, String> mReplacementMap;
 
-    private CalculatorExpressionTokenizer(Context context) {
+    public CalculatorExpressionTokenizer(Context context) {
         mReplacementMap = new HashMap<>();
 
         mReplacementMap.put(".", context.getString(R.string.dec_point));