Fix UI holes and bugs. Fix eval bugs.

Change layout to make the result display use a fixed font
size and limit the number of characters when it appears below the
formula.  This allows us to always get the proper expansion effect
and prevents scrolling from affecting the font size.

Add copy support for result display.

Add paste support for the formula.

Add keyboard input support.

Copy/paste can be used to remember old results in the calculator.
We save an identifying tag URI in the clip, in addition to text,
allowing us to paste old calculator results without precision
loss.

Copy/paste currently does not rely on selection at all.
I had trouble making it work that way in the formula.  It's
unclear that would be better, since we only allow copy of the
entire text and paste at the end.

Add a couple of alternate result display options to the
overflow menu.  (These appear quite useful, were trivial to
implement, and give us a better excuse for the overflow menu.)

Changed the behavior of the delete key in error state.
Changing it to CLEAR seemed unfriendly, since it prevents
corrections.  This is a change from L.

Made it clear that the CalculatorHitSomeButtons test is
currently 95% worthless.  It was apparentlly failing (due to test
infrastructure issues) but throwing an exception in a thread from
which it was not getting reported.  Decided to keep it, since I
would like a place to continue collecting regression tests, even
if we can't actually run them yet.

Includes some easy drive-by fixes for expression evaluation:

a) 2 / 2 * 3 was mis-parsed as 2 / (2 * 3).

b) Cosine evaluation had the sense of the test for a rational result reversed.

c) Constants without leading digits, like .1, are now handled correctly,
and decimal points in the formula are now internationalized.
(That's not yet true for the result.)

Change-Id: Ic24466b444b4a4633cfb036c67622c7f4fd644ec
diff --git a/res/layout/display.xml b/res/layout/display.xml
index 94f7848..b5a5b60 100644
--- a/res/layout/display.xml
+++ b/res/layout/display.xml
@@ -29,16 +29,24 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:inputType="text|textNoSuggestions"
+        android:textIsSelectable="false"
         android:textColor="@color/display_formula_text_color" />
 
+    <!--
+      We lay the result out to full width, but are careful to use only
+      2/3 of the space, so that we have room when we expand.
+      -->
+
     <com.android.calculator2.CalculatorResult
         android:id="@+id/result"
-        style="@style/DisplayEditTextStyle.Result"
+        style="@style/DisplayTextStyle.Result"
+        android:layout_alignParentRight="true"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_below="@id/formula"
         android:inputType="none"
-        android:focusable="false"
+        android:clickable="true"
+        android:textIsSelectable="false"
         android:textColor="@color/display_result_text_color" />
 
 </RelativeLayout>
diff --git a/res/layout/extras.xml b/res/layout/extras.xml
index 0af0704..713413c 100644
--- a/res/layout/extras.xml
+++ b/res/layout/extras.xml
@@ -18,11 +18,15 @@
 
 <!--
   TODO: Use framework Toolbar instead of custom overflow menu.
+  Together with setActionBar, that should also fix the COPY/PASTE
+  ugliness.
+  It is not immediately obvious how to get the layout inside the
+  Toolbar correct.
   -->
 
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/display"
+    android:id="@+id/toolbar"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:orientation="horizontal"
diff --git a/res/menu/menu.xml b/res/menu/copy.xml
similarity index 82%
copy from res/menu/menu.xml
copy to res/menu/copy.xml
index 16712ea..5897f88 100644
--- a/res/menu/menu.xml
+++ b/res/menu/copy.xml
@@ -19,10 +19,7 @@
 
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <item android:id="@+id/menu_help"
-        android:title="@string/help"/>
-
-    <item android:id="@+id/menu_about"
-        android:title="@string/about"/>
+    <item android:id="@+id/menu_copy"
+        android:title="@android:string/copy"/>
 
 </menu>
diff --git a/res/menu/menu.xml b/res/menu/overflow.xml
similarity index 83%
rename from res/menu/menu.xml
rename to res/menu/overflow.xml
index 16712ea..af5c7cb 100644
--- a/res/menu/menu.xml
+++ b/res/menu/overflow.xml
@@ -25,4 +25,10 @@
     <item android:id="@+id/menu_about"
         android:title="@string/about"/>
 
+    <item android:id="@+id/menu_leading"
+        android:title="@string/leading"/>
+
+    <item android:id="@+id/menu_fraction"
+        android:title="@string/fraction"/>
+
 </menu>
diff --git a/res/menu/menu.xml b/res/menu/paste.xml
similarity index 82%
copy from res/menu/menu.xml
copy to res/menu/paste.xml
index 16712ea..964be0d 100644
--- a/res/menu/menu.xml
+++ b/res/menu/paste.xml
@@ -19,10 +19,7 @@
 
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <item android:id="@+id/menu_help"
-        android:title="@string/help"/>
-
-    <item android:id="@+id/menu_about"
-        android:title="@string/about"/>
+    <item android:id="@+id/menu_paste"
+        android:title="@android:string/paste"/>
 
 </menu>
diff --git a/res/values-land/styles.xml b/res/values-land/styles.xml
index ac8a566..cc8e64c 100644
--- a/res/values-land/styles.xml
+++ b/res/values-land/styles.xml
@@ -26,12 +26,12 @@
         <item name="android:textSize">30sp</item>
     </style>
 
-    <style name="DisplayEditTextStyle.Result">
+    <style name="DisplayTextStyle.Result">
         <item name="android:paddingTop">8dip</item>
         <item name="android:paddingBottom">24dip</item>
         <item name="android:paddingStart">16dip</item>
         <item name="android:paddingEnd">16dip</item>
-        <item name="android:textSize">30sp</item>
+        <item name="android:textSize">28sp</item>
     </style>
 
     <style name="PadButtonStyle.Advanced">
diff --git a/res/values-port/styles.xml b/res/values-port/styles.xml
index ff6e303..348333f 100644
--- a/res/values-port/styles.xml
+++ b/res/values-port/styles.xml
@@ -28,12 +28,12 @@
         <item name="stepTextSize">8sp</item>
     </style>
 
-    <style name="DisplayEditTextStyle.Result">
+    <style name="DisplayTextStyle.Result">
         <item name="android:paddingTop">24dip</item>
         <item name="android:paddingBottom">48dip</item>
         <item name="android:paddingStart">16dip</item>
         <item name="android:paddingEnd">16dip</item>
-        <item name="android:textSize">36sp</item>
+        <item name="android:textSize">28sp</item>
     </style>
 
     <style name="PadButtonStyle.Advanced">
diff --git a/res/values-sw600dp-land/styles.xml b/res/values-sw600dp-land/styles.xml
index 050695d..21bdd90 100644
--- a/res/values-sw600dp-land/styles.xml
+++ b/res/values-sw600dp-land/styles.xml
@@ -26,12 +26,12 @@
         <item name="android:textSize">48sp</item>
     </style>
 
-    <style name="DisplayEditTextStyle.Result">
+    <style name="DisplayTextStyle.Result">
         <item name="android:paddingTop">26dip</item>
         <item name="android:paddingBottom">46dip</item>
         <item name="android:paddingStart">44dip</item>
         <item name="android:paddingEnd">44dip</item>
-        <item name="android:textSize">48sp</item>
+        <item name="android:textSize">36sp</item>
     </style>
 
     <style name="PadButtonStyle.Advanced">
diff --git a/res/values-sw600dp-port/styles.xml b/res/values-sw600dp-port/styles.xml
index 31f87c3..251c734 100644
--- a/res/values-sw600dp-port/styles.xml
+++ b/res/values-sw600dp-port/styles.xml
@@ -29,12 +29,12 @@
         <item name="stepTextSize">8sp</item>
     </style>
 
-    <style name="DisplayEditTextStyle.Result">
+    <style name="DisplayTextStyle.Result">
         <item name="android:paddingTop">32dip</item>
         <item name="android:paddingBottom">90dip</item>
         <item name="android:paddingStart">44dip</item>
         <item name="android:paddingEnd">44dip</item>
-        <item name="android:textSize">48sp</item>
+        <item name="android:textSize">36sp</item>
     </style>
 
     <style name="PadButtonStyle.Advanced">
diff --git a/res/values-sw800dp-land/styles.xml b/res/values-sw800dp-land/styles.xml
index e6d06b4..c88136d 100644
--- a/res/values-sw800dp-land/styles.xml
+++ b/res/values-sw800dp-land/styles.xml
@@ -28,12 +28,12 @@
         <item name="stepTextSize">8sp</item>
     </style>
 
-    <style name="DisplayEditTextStyle.Result">
+    <style name="DisplayTextStyle.Result">
         <item name="android:paddingTop">26dip</item>
         <item name="android:paddingBottom">46dip</item>
         <item name="android:paddingStart">44dip</item>
         <item name="android:paddingEnd">44dip</item>
-        <item name="android:textSize">56sp</item>
+        <item name="android:textSize">40sp</item>
     </style>
 
     <style name="PadButtonStyle.Advanced">
diff --git a/res/values-sw800dp-port/styles.xml b/res/values-sw800dp-port/styles.xml
index 6ed3886..3aec308 100644
--- a/res/values-sw800dp-port/styles.xml
+++ b/res/values-sw800dp-port/styles.xml
@@ -28,12 +28,12 @@
         <item name="stepTextSize">8sp</item>
     </style>
 
-    <style name="DisplayEditTextStyle.Result">
+    <style name="DisplayTextStyle.Result">
         <item name="android:paddingTop">32dip</item>
         <item name="android:paddingBottom">90dip</item>
         <item name="android:paddingStart">44dip</item>
         <item name="android:paddingEnd">44dip</item>
-        <item name="android:textSize">56sp</item>
+        <item name="android:textSize">40sp</item>
     </style>
 
     <style name="PadButtonStyle.Advanced">
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 9b018da..2b621bb 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -141,6 +141,8 @@
     <string name="desc_del">delete</string>
     <!-- Content description for '=' button. [CHAR_LIMIT=NONE] -->
     <string name="desc_eq">equals</string>
+    <!-- Toast shown when text is copied to the clipboard. -->
+    <string name="text_copied_toast">Text copied.</string>
 
     <!-- TODO: Revisit everything below here -->
     <!-- Displayed briefly to indicate not-yet-computed digit. -->
@@ -158,10 +160,18 @@
     <!-- Content description for overflow menu button. -->
     <string name="overflow_menu_description">overflow menu</string>
     <!-- The help message that's displayed in response to pushing the above button. -->
-    <string name="help_message">Use the keys to enter a standard arithmetic expression. It\'s fine to omit multiplication symbols and trailing parentheses.  The result displayed after hitting = is computed to an error of less than one in the last displayed digit.  Drag the display to see more digits.\n\nComputations involving infinite values may take forever.  Touch a button to terminate computation or wait for the timeout.</string>
+    <string name="help_message">Use the keys to enter a standard arithmetic expression. It\'s fine to omit multiplication symbols and trailing parentheses.  Long press delete key to clear. Drag the display to see more digits.\n\nComputations involving infinite values may take forever.  Wait for the timeout or touch a button to terminate computation.</string>
     <!-- Help message addendum for pager. -->
     <string name="help_pager">\n\nSwipe the keyboard to the left to see additional functions.</string>
     <!-- About menu entry; leads mostly to (English language!) copyright notice.  -->
     <string name="about">About &amp; Copyright</string>
+    <!-- Overflow menu entry to display result including leading digits. -->
+    <string name="leading">Answer with leading digits</string>
+    <!-- Overflow menu entry to display result as fraction. -->
+    <string name="fraction">Answer as fraction</string>
+    <!-- Appended indicator (for "leading" display) that result is exact. -->
+    <string name="exact">(exact)</string>
+    <!-- Indicator (for "leading" display) that result is inexact. -->
+    <string name="approximate">(±1 in last digit)</string>
 
 </resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 1732b73..843bf2d 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -32,6 +32,14 @@
         <item name="android:gravity">bottom|end</item>
     </style>
 
+    <style name="DisplayTextStyle" parent="@android:style/Widget.Material.Light.TextView">
+        <item name="android:background">@android:color/transparent</item>
+        <item name="android:cursorVisible">false</item>
+        <item name="android:fontFamily">sans-serif-light</item>
+        <item name="android:includeFontPadding">false</item>
+        <item name="android:gravity">bottom|end</item>
+    </style>
+
     <style name="PadButtonStyle" parent="@android:style/Widget.Material.Light.Button.Borderless">
         <item name="android:layout_width">wrap_content</item>
         <item name="android:layout_height">wrap_content</item>
diff --git a/src/com/android/calculator2/BoundedRational.java b/src/com/android/calculator2/BoundedRational.java
index 4a71dee..98d2564 100644
--- a/src/com/android/calculator2/BoundedRational.java
+++ b/src/com/android/calculator2/BoundedRational.java
@@ -14,10 +14,6 @@
  * limitations under the License.
  */
 
-// TODO: This is currently not hooked up to anything.  I started writing
-// it to capture my thoughts on heuristics for detecting specific exact
-// values.
-
 package com.android.calculator2;
 
 // We implement rational numbers of bounded size.
@@ -66,6 +62,16 @@
         return mNum.toString() + "/" + mDen.toString();
     }
 
+    // Output to user, more expensive, less useful for debugging
+    public String toNiceString() {
+        BoundedRational nicer = reduce().positive_den();
+        String result = nicer.mNum.toString();
+        if (!nicer.mDen.equals(BigInteger.ONE)) {
+            result += "/" + nicer.mDen;
+        }
+        return result;
+    }
+
     public static String toString(BoundedRational r) {
         if (r == null) return "not a small rational";
         return r.toString();
diff --git a/src/com/android/calculator2/Calculator.java b/src/com/android/calculator2/Calculator.java
index 2730e73..f5b9247 100644
--- a/src/com/android/calculator2/Calculator.java
+++ b/src/com/android/calculator2/Calculator.java
@@ -14,16 +14,19 @@
  * limitations under the License.
  */
 
-// TODO: Fix evaluation interface so the evaluator returns entire
-//       result, and display can properly handle variable width font.
-// TODO: Fix font handling and scaling in result display.
+// FIXME: Menu handling, particularly for cut/paste, is very ugly
+//        and not the way it was intended.
+//        Other menus are not handled brilliantly either.
+// TODO: Revisit handling of "Help" menu, so that it's more consistent
+//       with our conventions.
+// TODO: See if we can make scrolling look better, especially on small
+//       displays. Fix evaluation interface so the evaluator returns entire
+//       result, and formatting of exponent etc. is done separately.
+// TODO: Better indication of when the result is known to be exact.
 // TODO: Fix placement of inverse trig buttons.
-// TODO: Add Degree/Radian switch and display.
-// TODO: Handle physical keyboard correctly.
-// TODO: Fix internationalization, including result.
-// TODO: Check and fix accessability issues.
-// TODO: Support pasting of at least full result.  (Rounding?)
-// TODO: Copy/paste in formula.
+// TODO: Fix internationalization, particularly for result.
+// TODO: Check and possibly fix accessability issues.
+// TODO: Copy & more general paste in formula?
 
 package com.android.calculator2;
 
@@ -40,14 +43,20 @@
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.res.Resources;
+import android.graphics.Color;
 import android.graphics.Rect;
+import android.net.Uri;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
 import android.support.v4.view.ViewPager;
 import android.text.Editable;
+import android.text.SpannableString;
+import android.text.Spanned;
 import android.text.TextUtils;
 import android.text.TextWatcher;
+import android.text.style.ForegroundColorSpan;
 import android.util.Log;
+import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
 import android.view.Menu;
 import android.view.MenuItem;
@@ -75,7 +84,7 @@
 import java.text.DecimalFormatSymbols;  // TODO: May eventually not need this here.
 
 public class Calculator extends Activity
-        implements OnTextSizeChangeListener, OnLongClickListener, OnMenuItemClickListener {
+        implements OnTextSizeChangeListener, OnLongClickListener, OnMenuItemClickListener, CalculatorEditText.PasteListener {
 
     /**
      * Constant for an invalid resource id.
@@ -110,34 +119,44 @@
     // TODO: Possibly save a bit more information, e.g. its initial display string
     // or most significant digit position, to speed up restart.
 
-    private final TextWatcher mFormulaTextWatcher = new TextWatcher() {
-        @Override
-        public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
-        }
-
-        @Override
-        public void onTextChanged(CharSequence charSequence, int start, int count, int after) {
-        }
-
-        @Override
-        public void afterTextChanged(Editable editable) {
-            setState(CalculatorState.INPUT);
-            mEvaluator.evaluateAndShowResult();
-        }
-    };
+    // We currently assume that the formula does not change out from under us in
+    // any way. We explicitly handle all input to the formula here.
+    // TODO: Perhaps the formula should not be editable at all?
 
     private final OnKeyListener mFormulaOnKeyListener = new OnKeyListener() {
         @Override
         public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
+            if (keyEvent.getAction() != KeyEvent.ACTION_UP) return true;
             switch (keyCode) {
                 case KeyEvent.KEYCODE_NUMPAD_ENTER:
                 case KeyEvent.KEYCODE_ENTER:
-                    if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
-                        mCurrentButton = mEqualButton;
-                        onEquals();
-                    }
-                    // ignore all other actions
+                case KeyEvent.KEYCODE_DPAD_CENTER:
+                    mCurrentButton = mEqualButton;
+                    onEquals();
                     return true;
+                case KeyEvent.KEYCODE_DEL:
+                    mCurrentButton = mDeleteButton;
+                    onDelete();
+                    return true;
+                default:
+                    final int raw = keyEvent.getKeyCharacterMap()
+                          .get(keyCode, keyEvent.getMetaState());
+                    if ((raw & KeyCharacterMap.COMBINING_ACCENT) != 0) {
+                        return true; // discard
+                    }
+                    // Try to discard non-printing characters and the like.
+                    // The user will have to explicitly delete other junk that gets past us.
+                    if (Character.isIdentifierIgnorable(raw)
+                        || Character.isWhitespace(raw)) {
+                        return true;
+                    }
+                    char c = (char)raw;
+                    if (c == '=') {
+                        onEquals();
+                    } else {
+                        addChars(String.valueOf(c));
+                        redisplayAfterFormulaChange();
+                    }
             }
             return false;
         }
@@ -166,6 +185,10 @@
     private View mCurrentButton;
     private Animator mCurrentAnimator;
 
+    private String mUnprocessedChars = null;   // Characters that were recently entered
+                                               // at the end of the display that have not yet
+                                               // been added to the underlying expression.
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -204,9 +227,9 @@
                 }
             }
         }
-        mFormulaEditText.addTextChangedListener(mFormulaTextWatcher);
         mFormulaEditText.setOnKeyListener(mFormulaOnKeyListener);
         mFormulaEditText.setOnTextSizeChangeListener(this);
+        mFormulaEditText.setPasteListener(this);
         mDeleteButton.setOnLongClickListener(this);
         updateDegreeMode(mEvaluator.getDegreeMode());
         if (mCurrentState == CalculatorState.EVALUATE) {
@@ -217,11 +240,9 @@
         }
         if (mCurrentState != CalculatorState.INPUT) {
             setState(CalculatorState.INIT);
-            mEvaluator.evaluateAndShowResult();
             mEvaluator.requireResult();
         } else {
-            redisplayFormula();
-            mEvaluator.evaluateAndShowResult();
+            redisplayAfterFormulaChange();
         }
         // TODO: We're currently not saving and restoring scroll position.
         //       We probably should.  Details may require care to deal with:
@@ -248,6 +269,9 @@
         outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray());
     }
 
+    // Set the state, updating delete label and display colors.
+    // This restores display positions on moving to INPUT.
+    // But movement/animation for moving to RESULT has already been done. 
     private void setState(CalculatorState state) {
         if (mCurrentState != state) {
             if (state == CalculatorState.INPUT) {
@@ -255,8 +279,8 @@
             }
             mCurrentState = state;
 
-            if (mCurrentState == CalculatorState.RESULT
-                    || mCurrentState == CalculatorState.ERROR) {
+            if (mCurrentState == CalculatorState.RESULT) {
+                // No longer do this for ERROR; allow mistakes to be corrected.
                 mDeleteButton.setVisibility(View.GONE);
                 mClearButton.setVisibility(View.VISIBLE);
             } else {
@@ -320,6 +344,32 @@
         }
     }
 
+    // Add the given button id to input expression.
+    // If appropriate, clear the expression before doing so.
+    private void addKeyToExpr(int id) {
+        if (mCurrentState == CalculatorState.ERROR) {
+            setState(CalculatorState.INPUT);
+        } else if (mCurrentState == CalculatorState.RESULT) {
+            if (KeyMaps.isBinary(id) || KeyMaps.isSuffix(id)) {
+                mEvaluator.collapse();
+            } else {
+                mEvaluator.clear();
+            }
+            setState(CalculatorState.INPUT);
+        }
+        if (!mEvaluator.append(id)) {
+            // TODO: Some user visible feedback?
+        }
+    }
+
+    private void redisplayAfterFormulaChange() {
+        // TODO: Could do this more incrementally.
+        redisplayFormula();
+        setState(CalculatorState.INPUT);
+        mResult.clear();
+        mEvaluator.evaluateAndShowResult();
+    }
+
     public void onButtonClick(View view) {
         mCurrentButton = view;
         int id = view.getId();
@@ -362,30 +412,25 @@
                 mEvaluator.evaluateAndShowResult();
                 break;
             default:
-                if (mCurrentState == CalculatorState.ERROR) {
-                    setState(CalculatorState.INPUT);
-                }
-                if (mCurrentState == CalculatorState.RESULT) {
-                    if (KeyMaps.isBinary(id) || KeyMaps.isSuffix(id)) {
-                        mEvaluator.collapse();
-                    } else {
-                        mEvaluator.clear();
-                    }
-                }
-                if (!mEvaluator.append(id)) {
-                    // TODO: Some user visible feedback?
-                }
-                // TODO: Could do this more incrementally.
-                redisplayFormula();
-                setState(CalculatorState.INPUT);
-                mResult.clear();
-                mEvaluator.evaluateAndShowResult();
+                addKeyToExpr(id);
+                redisplayAfterFormulaChange();
                 break;
         }
     }
 
     void redisplayFormula() {
-        mFormulaEditText.setText(mEvaluator.getExpr().toString(this));
+        String formula = mEvaluator.getExpr().toString(this);
+        if (mUnprocessedChars != null) {
+            // Add and highlight characters we couldn't process.
+            SpannableString formatted = new SpannableString(formula + mUnprocessedChars);
+            // TODO: should probably match this to the error color.
+            formatted.setSpan(new ForegroundColorSpan(Color.RED),
+                              formula.length(), formatted.length(),
+                              Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+            mFormulaEditText.setText(formatted);
+        } else {
+            mFormulaEditText.setText(formula);
+        }
     }
 
     @Override
@@ -407,7 +452,6 @@
         } else { // in EVALUATE or INIT state
             mResult.displayResult(initDisplayPrec, truncatedWholeNumber);
             onResult(mCurrentState != CalculatorState.INIT);
-            setState(CalculatorState.RESULT);
         }
     }
 
@@ -457,12 +501,24 @@
     }
 
     private void onDelete() {
-        // Delete works like backspace; remove the last character from the expression.
+        // Delete works like backspace; remove the last character or operator from the expression.
+        // Note that we handle keyboard delete exactly like the delete button.  For
+        // example the delete button can be used to delete a character from an incomplete
+        // function name typed on a physical keyboard.
         mEvaluator.cancelAll();
-        mEvaluator.getExpr().delete();
-        redisplayFormula();
-        mResult.clear();
-        mEvaluator.evaluateAndShowResult();
+        // This should be impossible in RESULT state.
+        setState(CalculatorState.INPUT);
+        if (mUnprocessedChars != null) {
+            int len = mUnprocessedChars.length();
+            if (len > 0) {
+                mUnprocessedChars = mUnprocessedChars.substring(0, len-1);
+            } else {
+                mEvaluator.getExpr().delete();
+            }
+        } else {
+            mEvaluator.getExpr().delete();
+        }
+        redisplayAfterFormulaChange();
     }
 
     private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
@@ -523,6 +579,7 @@
         if (mEvaluator.getExpr().isEmpty()) {
             return;
         }
+        mUnprocessedChars = null;
         mResult.clear();
         mEvaluator.clear();
         reveal(mCurrentButton, R.color.calculator_accent_color, new AnimatorListenerAdapter() {
@@ -559,21 +616,21 @@
     // so that we can continue to properly support scrolling of the result.
     // We assume the result already contains the text to be expanded.
     private void onResult(boolean animate) {
-        // Calculate the values needed to perform the scale and translation animations,
-        // accounting for how the scale will affect the final position of the text.
-        // We want to fix the character size in the display to avoid weird effects
+        // Calculate the values needed to perform the scale and translation animations.
+        // We now fix the character size in the display to avoid weird effects
         // when we scroll.
-        final float resultScale =
-                mFormulaEditText.getVariableTextSize(mResult.getText().toString())
-                                                     / mResult.getTextSize() - 0.1f;
-        // FIXME:  This doesn't work correctly.  The -0.1 is a fudge factor to
-        // improve things slightly.  Remove when fixed.
-        final float resultTranslationX = (1.0f - resultScale) *
-                (mResult.getWidth() / 2.0f - mResult.getPaddingEnd());
-        final float resultTranslationY = (1.0f - resultScale) *
-                (mResult.getHeight() / 2.0f - mResult.getPaddingBottom()) +
-                (mFormulaEditText.getBottom() - mResult.getBottom()) +
-                (mResult.getPaddingBottom() - mFormulaEditText.getPaddingBottom());
+        // Display.xml is designed to ensure exactly a 3/2 ratio between the formula
+        // slot and small result slot.
+        final float resultScale = 1.5f;
+        final float resultTranslationX = -mResult.getWidth() * (resultScale - 1)/2;
+                // mFormulaEditText is aligned with mResult on the right.
+                // When we enlarge it around its center, the right side
+                // moves to the right.  This compensates.
+        float resultTranslationY = -mResult.getHeight();
+        // This is how much we want to move the bottom.
+        // Now compensate for the fact that we're
+        // simultaenously expanding it around its center by half its height
+        resultTranslationY += mResult.getHeight() * (resultScale-1)/2;
         final float formulaTranslationY = -mFormulaEditText.getBottom();
 
         // TODO: Reintroduce textColorAnimator?
@@ -614,6 +671,7 @@
             mResult.setTranslationX(resultTranslationX);
             mResult.setTranslationY(resultTranslationY);
             mFormulaEditText.setTranslationY(formulaTranslationY);
+            setState(CalculatorState.RESULT);
         }
     }
 
@@ -636,8 +694,14 @@
     private PopupMenu constructPopupMenu() {
         final PopupMenu popupMenu = new PopupMenu(this, mOverflowMenuButton);
         mOverflowMenuButton.setOnTouchListener(popupMenu.getDragToOpenListener());
+        popupMenu.inflate(R.menu.overflow);
         final Menu menu = popupMenu.getMenu();
-        popupMenu.inflate(R.menu.menu);
+        if (mCurrentState != CalculatorState.RESULT) {
+            menu.findItem(R.id.menu_fraction).setEnabled(false);
+            menu.findItem(R.id.menu_leading).setEnabled(false);
+        } else if (mEvaluator.getRational() == null) {
+            menu.findItem(R.id.menu_fraction).setEnabled(false);
+        }
         popupMenu.setOnMenuItemClickListener(this);
         onPrepareOptionsMenu(menu);
         return popupMenu;
@@ -652,26 +716,53 @@
             case R.id.menu_about:
                 displayAboutPage();
                 return true;
+            case R.id.menu_fraction:
+                displayFraction();
+                return true;
+            case R.id.menu_leading:
+                displayFull();
+                return true;
             default:
                 return super.onOptionsItemSelected(item);
         }
     }
 
-    private void displayHelpMessage() {
+    private void displayMessage(String s) {
         AlertDialog.Builder builder = new AlertDialog.Builder(this);
-        if (mPadViewPager != null) {
-            builder.setMessage(getResources().getString(R.string.help_message)
-                               + getResources().getString(R.string.help_pager));
-        } else {
-            builder.setMessage(R.string.help_message);
-        }
-        builder.setNegativeButton(R.string.dismiss,
+        builder.setMessage(s)
+               .setNegativeButton(R.string.dismiss,
                     new DialogInterface.OnClickListener() {
                         public void onClick(DialogInterface d, int which) { }
                     })
                .show();
     }
 
+    private void displayHelpMessage() {
+        Resources res = getResources();
+        String msg = res.getString(R.string.help_message);
+        if (mPadViewPager != null) {
+            msg += res.getString(R.string.help_pager);
+        }
+        displayMessage(msg);
+    }
+
+    private void displayFraction() {
+        BoundedRational result = mEvaluator.getRational();
+        displayMessage(result.toNiceString());
+    }
+
+    // Display full result to currently evaluated precision
+    private void displayFull() {
+        Resources res = getResources();
+        String msg = mResult.getFullText() + " ";
+        if (mResult.fullTextIsExact()) {
+            msg += res.getString(R.string.exact);
+        } else {
+            msg += res.getString(R.string.approximate);
+        }
+        displayMessage(msg);
+    }
+
     private void displayAboutPage() {
         WebView wv = new WebView(this);
         wv.loadUrl("file:///android_asset/about.txt");
@@ -684,57 +775,68 @@
                 .show();
     }
 
-    // TODO: Probably delete the following method and all of its callers before release.
-    //       Definitely delete most of its callers.
-    private static final String LOG_TAG = "Calculator";
-
-    static void log(String message) {
-        Log.v(LOG_TAG, message);
+    // Add input characters to the end of the expression by mapping them to
+    // the appropriate button pushes when possible.  Leftover characters
+    // are added to mUnprocessedChars, which is presumed to immediately
+    // precede the newly added characters.
+    private void addChars(String moreChars) {
+        if (mUnprocessedChars != null) {
+            moreChars = mUnprocessedChars + moreChars;
+        }
+        int current = 0;
+        int len = moreChars.length();
+        while (current < len) {
+            char c = moreChars.charAt(current);
+            int k = KeyMaps.keyForChar(c, this);
+            if (k != View.NO_ID) {
+                mCurrentButton = findViewById(k);
+                addKeyToExpr(k);
+                if (Character.isSurrogate(c)) {
+                    current += 2;
+                } else {
+                    ++current;
+                }
+                continue;
+            }
+            int f = KeyMaps.funForString(moreChars, current, this);
+            if (f != View.NO_ID) {
+                mCurrentButton = findViewById(f);
+                addKeyToExpr(f);
+                if (f == R.id.op_sqrt) {
+                    // Square root entered as function; don't lose the parenthesis.
+                    addKeyToExpr(R.id.lparen);
+                }
+                current = moreChars.indexOf('(', current) + 1;
+                continue;
+            }
+            // There are characters left, but we can't convert them to button presses.
+            mUnprocessedChars = moreChars.substring(current);
+            redisplayAfterFormulaChange();
+            return;
+        }
+        mUnprocessedChars = null;
+        redisplayAfterFormulaChange();
+        return;
     }
 
-    // EVERYTHING BELOW HERE was preserved from the KitKat version of the
-    // calculator, since we expect to need it again once functionality is a bit more
-    // more complete.  But it has not yet been wired in correctly, and
-    // IS CURRENTLY UNUSED.
-
-    // Is s a valid constant?
-    // TODO: Possibly generalize to scientific notation, hexadecimal, etc.
-    static boolean isConstant(CharSequence s) {
-        boolean sawDecimal = false;
-        boolean sawDigit = false;
-        final char decimalPt = DecimalFormatSymbols.getInstance().getDecimalSeparator();
-        int len = s.length();
-        int i = 0;
-        while (i < len && Character.isWhitespace(s.charAt(i))) ++i;
-        if (i < len && s.charAt(i) == '-') ++i;
-        for (; i < len; ++i) {
-            char c = s.charAt(i);
-            if (c == '.' || c == decimalPt) {
-                if (sawDecimal) return false;
-                sawDecimal = true;
-            } else if (Character.isDigit(c)) {
-                sawDigit = true;
-            } else {
-                break;
+    @Override
+    public boolean paste(Uri uri) {
+        if (mEvaluator.isLastSaved(uri)) {
+            if (mCurrentState == CalculatorState.ERROR
+                || mCurrentState == CalculatorState.RESULT) {
+                setState(CalculatorState.INPUT);
+                mEvaluator.clear();
             }
+            mEvaluator.addSaved();
+            redisplayAfterFormulaChange();
+            return true;
         }
-        while (i < len && Character.isWhitespace(s.charAt(i))) ++i;
-        return i == len && sawDigit;
+        return false;
     }
 
-    // Paste a valid character sequence representing a constant.
-    void paste(CharSequence s) {
-        mEvaluator.cancelAll();
-        if (mCurrentState == CalculatorState.RESULT) {
-            mEvaluator.clear();
-        }
-        int len = s.length();
-        for (int i = 0; i < len; ++i) {
-            char c = s.charAt(i);
-            if (!Character.isWhitespace(c)) {
-                mEvaluator.append(KeyMaps.keyForChar(c));
-            }
-        }
+    @Override
+    public void paste(String s) {
+        addChars(s);
     }
 
 }
diff --git a/src/com/android/calculator2/CalculatorEditText.java b/src/com/android/calculator2/CalculatorEditText.java
index 380aa81..4ff9678 100644
--- a/src/com/android/calculator2/CalculatorEditText.java
+++ b/src/com/android/calculator2/CalculatorEditText.java
@@ -16,18 +16,24 @@
 
 package com.android.calculator2;
 
+import android.content.ClipboardManager;
+import android.content.ClipData;
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Paint;
 import android.graphics.Paint.FontMetricsInt;
 import android.graphics.Rect;
+import android.net.Uri;
 import android.os.Parcelable;
 import android.text.method.ScrollingMovementMethod;
 import android.text.TextPaint;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.util.TypedValue;
 import android.view.ActionMode;
+import android.view.GestureDetector;
 import android.view.Menu;
+import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.MotionEvent;
 import android.widget.EditText;
@@ -39,15 +45,31 @@
 
 public class CalculatorEditText extends EditText {
 
-    private final static ActionMode.Callback NO_SELECTION_ACTION_MODE_CALLBACK =
+
+    private final ActionMode.Callback mPasteActionModeCallback =
             new ActionMode.Callback() {
         @Override
         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
-            return false;
+            switch (item.getItemId()) {
+            case R.id.menu_paste:
+                pasteContent();
+                mode.finish();
+                return true;
+            default:
+                return false;
+            }
         }
 
         @Override
         public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+            ClipboardManager clipboard =
+                (ClipboardManager) getContext().getSystemService(
+                        Context.CLIPBOARD_SERVICE);
+            if (clipboard.hasPrimaryClip()) {
+                MenuInflater inflater = mode.getMenuInflater();
+                inflater.inflate(R.menu.paste, menu);
+                return true;
+            }
             // Prevents the selection action mode on double tap.
             return false;
         }
@@ -62,6 +84,25 @@
         }
     };
 
+    private PasteListener mPasteListener;
+
+    public void setPasteListener(PasteListener pasteListener) {
+        mPasteListener = pasteListener;
+    }
+
+    private void pasteContent() {
+        ClipboardManager clipboard =
+                (ClipboardManager) getContext().getSystemService(
+                        Context.CLIPBOARD_SERVICE);
+        ClipData cd = clipboard.getPrimaryClip();
+        ClipData.Item item = cd.getItemAt(0);
+        // TODO: Should we handle multiple selections?
+        Uri uri = item.getUri();
+        if (uri == null || !mPasteListener.paste(uri)) {
+            mPasteListener.paste(item.coerceToText(getContext()).toString());
+        }
+    }
+
     private final float mMaximumTextSize;
     private final float mMinimumTextSize;
     private final float mStepTextSize;
@@ -73,6 +114,14 @@
     private int mWidthConstraint = -1;
     private OnTextSizeChangeListener mOnTextSizeChangeListener;
 
+    final GestureDetector mLongTouchDetector =
+        new GestureDetector(new GestureDetector.SimpleOnGestureListener() {
+            @Override
+            public void onLongPress(MotionEvent e) {
+                startActionMode(mPasteActionModeCallback);
+            }
+        });
+
     public CalculatorEditText(Context context) {
         this(context, null);
     }
@@ -95,7 +144,9 @@
 
         a.recycle();
 
-        setCustomSelectionActionModeCallback(NO_SELECTION_ACTION_MODE_CALLBACK);
+        // Paste ActionMode is triggered explicitly, not through
+        // setCustomSelectionActionModeCallback.
+
         if (isFocusable()) {
             setMovementMethod(ScrollingMovementMethod.getInstance());
         }
@@ -104,13 +155,9 @@
     }
 
     @Override
-    public boolean onTouchEvent(MotionEvent event) {
-        if (event.getActionMasked() == MotionEvent.ACTION_UP) {
-            // Hack to prevent keyboard and insertion handle from showing.
-            cancelLongPress();
-        }
-        return super.onTouchEvent(event);
-    }
+    public boolean onTouchEvent(MotionEvent e) {
+        return mLongTouchDetector.onTouchEvent(e);
+    };
 
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
@@ -204,4 +251,9 @@
     public interface OnTextSizeChangeListener {
         void onTextSizeChanged(TextView textView, float oldSize);
     }
+
+    public interface PasteListener {
+        void paste(String s);
+        boolean paste(Uri u);
+    }
 }
diff --git a/src/com/android/calculator2/CalculatorExpr.java b/src/com/android/calculator2/CalculatorExpr.java
index 2e7dee5..90cd8f1 100644
--- a/src/com/android/calculator2/CalculatorExpr.java
+++ b/src/com/android/calculator2/CalculatorExpr.java
@@ -16,13 +16,6 @@
 
 package com.android.calculator2;
 
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.IdentityHashMap;
-import java.math.BigInteger;
-import java.io.DataInput;
-import java.io.DataOutput;
-import java.io.IOException;
 
 import com.hp.creals.CR;
 import com.hp.creals.UnaryCRFunction;
@@ -31,6 +24,15 @@
 
 import android.content.Context;
 
+import java.math.BigInteger;
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.text.DecimalFormatSymbols;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+
 // A mathematical expression represented as a sequence of "tokens".
 // Many tokes are represented by button ids for the corresponding operator.
 // Parsed only when we evaluate the expression using the "eval" method.
@@ -137,10 +139,26 @@
             return (mSawDecimal == false && mWhole.isEmpty());
         }
 
+        // Produces human-readable string, as typed.
+        // Decimal separator is mapped to canonical character.
         @Override
         public String toString() {
             String result = mWhole;
             if (mSawDecimal) {
+                result += DecimalFormatSymbols.getInstance()
+                                              .getDecimalSeparator();
+                result += mFraction;
+            }
+            return result;
+        }
+
+        // Eliminates leading decimal, which some of our
+        // other packages don't like.
+        // Doesn't internationalize decimnal point.
+        public String toNiceString() {
+            String result = mWhole;
+            if (result.isEmpty()) result = "0";
+            if (mSawDecimal) {
                 result += '.';
                 result += mFraction;
             }
@@ -148,7 +166,9 @@
         }
 
         public BoundedRational toRational() {
-            BigInteger num = new BigInteger(mWhole + mFraction);
+            String whole = mWhole;
+            if (whole.isEmpty()) whole = "0";
+            BigInteger num = new BigInteger(whole + mFraction);
             BigInteger den = BigInteger.TEN.pow(mFraction.length());
             return new BoundedRational(num, den);
         }
@@ -523,7 +543,7 @@
         CR value;
         if (t instanceof Constant) {
             Constant c = (Constant)t;
-            value = CR.valueOf(c.toString(),10);
+            value = CR.valueOf(c.toNiceString(),10);
             return new EvalRet(i+1, value, c.toRational());
         }
         if (t instanceof PreEval) {
@@ -561,7 +581,7 @@
             if (isOperator(argVal.mPos, R.id.rparen)) argVal.mPos++;
             ratVal = ec.mDegreeMode? BoundedRational.degreeCos(argVal.mRatVal)
                                      : BoundedRational.cos(argVal.mRatVal);
-            if (ratVal == null) break;
+            if (ratVal != null) break;
             return new EvalRet(argVal.mPos,
                     toRadians(argVal.mVal,ec).cos(), null);
         case R.id.fun_tan:
@@ -741,7 +761,7 @@
                || (is_div = isOperator(cpos, R.id.op_div))
                || canStartFactor(cpos)) {
             if (is_mul || is_div) ++cpos;
-            tmp = evalTerm(cpos, ec);
+            tmp = evalSignedFactor(cpos, ec);
             if (is_div) {
                 ratVal = BoundedRational.divide(ratVal, tmp.mRatVal);
                 if (ratVal == null) {
diff --git a/src/com/android/calculator2/CalculatorResult.java b/src/com/android/calculator2/CalculatorResult.java
index 6c727e0..fb26b5c 100644
--- a/src/com/android/calculator2/CalculatorResult.java
+++ b/src/com/android/calculator2/CalculatorResult.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2014 The Android Open Source Project
+ * Copyright (C) 2015 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.
@@ -16,28 +16,38 @@
 
 package com.android.calculator2;
 
-import android.widget.TextView;
+import android.content.ClipboardManager;
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.Context;
 import android.graphics.Typeface;
 import android.graphics.Paint;
 import android.graphics.Rect;
 import android.graphics.Color;
+import android.net.Uri;
+import android.widget.TextView;
 import android.widget.OverScroller;
-import android.view.GestureDetector;
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.View;
 import android.text.Editable;
 import android.text.Spanned;
 import android.text.SpannableString;
 import android.text.style.ForegroundColorSpan;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.ActionMode;
+import android.view.GestureDetector;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.Toast;
 
 import android.support.v4.view.ViewCompat;
 
 
 // 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 CalculatorEditText {
+public class CalculatorResult extends TextView {
     final static int MAX_RIGHT_SCROLL = 100000000;
     final static int INVALID = MAX_RIGHT_SCROLL + 10000;
         // A larger value is unlikely to avoid running out of space
@@ -63,11 +73,21 @@
     private int mLastPos;   // Position already reflected in display.
     private int mMinPos;    // Maximum position before all digits
                             // digits disappear of the right.
-    private int mCharWidth; // Use monospaced font for now.
-                            // This shouldn't be much harder with a variable
-                            // width font, except it may be even less smooth
-        // FIXME: This is not really a fixed width font anymore.
+    private Object mWidthLock = new Object();
+                            // Protects the next two fields.
+    private int mWidthConstraint = -1;
+                            // Our total width in pixels.
+    private int mCharWidth = 1;
+                            // Maximum character width.
+                            // For now we pretend that all characters
+                            // have this width.
+                            // TODO: We're not really using a fixed
+                            // width font.  But it appears to be close
+                            // enough for the characters we use that
+                            // the difference is not noticeable.
     private Paint mPaint;   // Paint object matching display.
+    private static final int MAX_WIDTH = 100;
+                            // Maximum number of digits displayed
 
     public CalculatorResult(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -106,25 +126,48 @@
                     ViewCompat.postInvalidateOnAnimation(CalculatorResult.this);
                     return true;
                 }
+                @Override
+                public void onLongPress(MotionEvent e) {
+                    startActionMode(mCopyActionModeCallback);
+                }
             });
         setOnTouchListener(mTouchListener);
         setHorizontallyScrolling(false);  // do it ourselves
         setCursorVisible(false);
-        setTypeface(Typeface.MONOSPACE);
-        mPaint = getPaint();
-        mCharWidth = (int) mPaint.measureText("5");
+
+        // Copy ActionMode is triggered explicitly, not through
+        // setCustomSelectionActionModeCallback.
     }
 
     void setEvaluator(Evaluator evaluator) {
         mEvaluator = evaluator;
     }
 
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        mPaint = getPaint();
+        // We assume that "5" has maximal width.  We measure a
+        // long string to make sure that spaces are included.
+        StringBuilder sb = new StringBuilder(MAX_WIDTH);
+        for (int i = 0; i < MAX_WIDTH; ++i) {
+            sb.append('5');
+        }
+        synchronized(mWidthLock) {
+            mWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
+                                - getPaddingLeft() - getPaddingRight();
+            mCharWidth = (int)Math.ceil(mPaint.measureText(sb.toString())
+                                    / MAX_WIDTH);
+        }
+    }
+
     // Display a new result, given initial displayed
     // precision and the string representing the whole part of
     // the number to be displayed.
     // We pass the string, instead of just the length, so we have
-    // one less place to fix in case we ever decide to use a variable
-    // width font.
+    // one less place to fix in case we ever decide to
+    // correctly use a variable width font.
     void displayResult(int initPrec, String truncatedWholePart) {
         mLastPos = INVALID;
         mCurrentPos = initPrec * mCharWidth;
@@ -132,32 +175,47 @@
         redisplay();
     }
 
-    // May be called from non-UI thread, but after initialization.
-    int getCharWidth() {
-        return mCharWidth;
-    }
-
     void displayError(int resourceId) {
         mScrollable = false;
         setText(resourceId);
     }
 
     // Return entire result (within reason) up to current displayed precision.
-    public CharSequence getFullText() {
-        if (!mScrollable) return getText();
+    public String getFullText() {
+        if (!mScrollable) return getText().toString();
         int currentCharPos = mCurrentPos/mCharWidth;
         return mEvaluator.getString(currentCharPos, 1000000);
     }
 
+    public boolean fullTextIsExact() {
+        BoundedRational rat = mEvaluator.getRational();
+        int currentCharPos = mCurrentPos/mCharWidth;
+        if (currentCharPos == -1) {
+            // Suppressing decimal point; still showing all
+            // integral digits.
+            currentCharPos = 0;
+        }
+        // TODO: Could handle scientific notation cases better;
+        // We currently treat those conservatively as approximate.
+        return (currentCharPos >= BoundedRational.digitsRequired(rat));
+    }
+
+    // May be called asynchronously from non-UI thread.
     int getMaxChars() {
-        int result = getWidthConstraint() / mCharWidth;
-        // FIXME: We can apparently finish evaluating before 
-        // onMeasure in CalculatorEditText has been called, in
-        // which case we get 0 or -1 as the width constraint.
-        // Perhaps guess conservatively here and reevaluate
-        // in InitialResult.onPostExecute?
+        // We only use 2/3 of the available space, since the
+        // left 1/3 of the result is not visible when it is shown
+        // in large size.
+        int result;
+        synchronized(mWidthLock) {
+            result = 2 * mWidthConstraint / (3 * mCharWidth);
+            // We can apparently finish evaluating before
+            // onMeasure in CalculatorEditText has been called, in
+            // which case we get 0 or -1 as the width constraint.
+        }
         if (result <= 0) {
-            return 8;
+            // Return something conservatively big, to force sufficient
+            // evaluation.
+            return MAX_WIDTH;
         } else {
             return result;
         }
@@ -176,7 +234,7 @@
         if (epos > 0 && result.indexOf('.') == -1) {
           // Gray out exponent if used as position indicator
             SpannableString formattedResult = new SpannableString(result);
-            formattedResult.setSpan(new ForegroundColorSpan(Color.GRAY),
+            formattedResult.setSpan(new ForegroundColorSpan(Color.LTGRAY),
                                     epos, result.length(),
                                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
             setText(formattedResult);
@@ -201,4 +259,61 @@
         }
     }
 
+    // Copy support:
+
+    private ActionMode.Callback mCopyActionModeCallback =
+                                new ActionMode.Callback() {
+        @Override
+        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+            MenuInflater inflater = mode.getMenuInflater();
+            inflater.inflate(R.menu.copy, menu);
+            return true;
+        }
+
+        @Override
+        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+            return false; // Return false if nothing is done
+        }
+
+        @Override
+        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+            switch (item.getItemId()) {
+            case R.id.menu_copy:
+                copyContent();
+                mode.finish();
+                return true;
+            default:
+                return false;
+            }
+        }
+
+        @Override
+        public void onDestroyActionMode(ActionMode mode) {
+        }
+    };
+
+    private void setPrimaryClip(ClipData clip) {
+        ClipboardManager clipboard = (ClipboardManager) getContext().
+                getSystemService(Context.CLIPBOARD_SERVICE);
+        clipboard.setPrimaryClip(clip);
+    }
+
+    private void copyContent() {
+        final CharSequence text = getFullText();
+        ClipboardManager clipboard =
+                (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());
+        String[] mimeTypes =
+                new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN};
+        ClipData cd = new ClipData("calculator result",
+                                   mimeTypes, newItem);
+        clipboard.setPrimaryClip(cd);
+        Toast.makeText(getContext(), R.string.text_copied_toast,
+                       Toast.LENGTH_SHORT).show();
+    }
+
 }
diff --git a/src/com/android/calculator2/Evaluator.java b/src/com/android/calculator2/Evaluator.java
index 56a80da..8b162c2 100644
--- a/src/com/android/calculator2/Evaluator.java
+++ b/src/com/android/calculator2/Evaluator.java
@@ -69,36 +69,46 @@
 
 package com.android.calculator2;
 
-import android.text.TextUtils;
-import android.view.KeyEvent;
-import android.widget.EditText;
-import android.content.Context;
-import android.content.res.Resources;
-import android.os.AsyncTask;
 import android.app.AlertDialog;
 import android.content.DialogInterface;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.AsyncTask;
 import android.os.Handler;
-
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-import java.math.BigInteger;
-
-import java.io.DataInput;
-import java.io.DataOutput;
-import java.io.IOException;
-import java.text.DecimalFormatSymbols;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.widget.EditText;
 
 import com.hp.creals.CR;
 import com.hp.creals.PrecisionOverflowError;
 import com.hp.creals.AbortedError;
 
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.SimpleDateFormat;
+import java.math.BigInteger;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map.Entry;
+import java.util.Random;
+import java.util.Set;
+import java.util.TimeZone;
+
 class Evaluator {
     private final Calculator mCalculator;
     private final CalculatorResult mResult;  // The result display View
     private CalculatorExpr mExpr;      // Current calculator expression
+    private CalculatorExpr mSaved;     // Last saved expression.
+                                       // Either null or contains a single
+                                       // preevaluated node.
+    private String mSavedName;         // A hopefully unique name associated
+                                       // with mSaved.
     // The following are valid only if an evaluation
     // completed successfully.
         private CR mVal;               // value of mExpr as constructive real
@@ -169,6 +179,8 @@
         mCalculator = calculator;
         mResult = resultDisplay;
         mExpr = new CalculatorExpr();
+        mSaved = new CalculatorExpr();
+        mSavedName = "none";
         mTimeoutHandler = new Handler();
         mDegreeMode = false;  // Remain compatible with previous versions.
     }
@@ -381,7 +393,24 @@
             mLastDigs = result.mInitDisplayPrec;
             int dotPos = mCache.indexOf('.');
             String truncatedWholePart = mCache.substring(0, dotPos);
-            mCalculator.onEvaluate(result.mInitDisplayPrec,truncatedWholePart);
+            // 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 init_prec = result.mInitDisplayPrec;
+            int msd = getMsdPos(mCache);
+            int new_init_prec = getPreferredPrec(mCache, msd,
+                                BoundedRational.digitsRequired(mRatVal));
+            if (new_init_prec < init_prec) {
+                init_prec = new_init_prec;
+            } 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(init_prec,truncatedWholePart);
         }
         @Override
         protected void onCancelled(InitialResult result) {
@@ -657,6 +686,11 @@
             return res;
     }
 
+    // Return rational representation of current result, if any.
+    public BoundedRational getRational() {
+        return mRatVal;
+    }
+
     private void clearCache() {
         mCache = null;
         mCacheDigs = mCacheDigsReq = 0;
@@ -699,7 +733,6 @@
     // leaving the expression displayed.
     boolean cancelAll() {
         if (mCurrentReevaluator != null) {
-            Calculator.log("Cancelling reevaluator");
             mCurrentReevaluator.cancel(true);
             mCacheDigsReq = mCacheDigs;
             // Backgound computation touches only constructive reals.
@@ -707,7 +740,6 @@
             mCurrentReevaluator = null;
         }
         if (mEvaluator != null) {
-            Calculator.log("Cancelling evaluator");
             mEvaluator.cancel(true);
             // There seems to be no good way to wait for cancellation
             // to complete, and the evaluation continues to look at
@@ -727,8 +759,10 @@
             CalculatorExpr.initExprInput();
             mDegreeMode = in.readBoolean();
             mExpr = new CalculatorExpr(in);
+            mSavedName = in.readUTF();
+            mSaved = new CalculatorExpr(in);
         } catch (IOException e) {
-            Calculator.log("" + e);
+            Log.v("Calculator", "Exception while restoring:\n" + e);
         }
     }
 
@@ -737,8 +771,10 @@
             CalculatorExpr.initExprOutput();
             out.writeBoolean(mDegreeMode);
             mExpr.write(out);
+            out.writeUTF(mSavedName);
+            mSaved.write(out);
         } catch (IOException e) {
-            Calculator.log("" + e);
+            Log.v("Calculator", "Exception while saving state:\n" + e);
         }
     }
 
@@ -776,6 +812,51 @@
         mExpr.append(abbrvExpr);
     }
 
+    // Same as above, but put result in mSaved, leaving mExpr alone.
+    // Return false if result is unavailable.
+    boolean collapseToSaved() {
+        if (mCache == null) return false;
+        BigInteger intVal = BoundedRational.asBigInteger(mRatVal);
+        CalculatorExpr abbrvExpr = mExpr.abbreviate(
+                                      mVal, mRatVal, mDegreeMode,
+                                      getShortString(mCache, intVal));
+        mSaved.clear();
+        mSaved.append(abbrvExpr);
+        return true;
+    }
+
+    Uri uriForSaved() {
+        return new Uri.Builder().scheme("tag")
+                                .encodedOpaquePart(mSavedName)
+                                .build();
+    }
+
+    // Collapse the current expression to mSaved and return a URI
+    // describing this particular result, so that we can refer to it
+    // later.
+    Uri capture() {
+        if (!collapseToSaved()) return null;
+        // Generate a new (entirely private) URI for this result.
+        // Attempt to conform to RFC4151, though it's unclear it matters.
+        Date date = new Date();
+        TimeZone tz = TimeZone.getDefault();
+        DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
+        df.setTimeZone(tz);
+        String isoDate = df.format(new Date());
+        mSavedName = "calculator2.android.com," + isoDate + ":"
+                     + (new Random().nextInt() & 0x3fffffff);
+        Uri tag = uriForSaved();
+        return tag;
+    }
+
+    boolean isLastSaved(Uri uri) {
+        return uri.equals(uriForSaved());
+    }
+
+    void addSaved() {
+        mExpr.append(mSaved);
+    }
+
     // Retrieve the main expression being edited.
     // It is the callee's reponsibility to call cancelAll to cancel
     // ongoing concurrent computations before modifying the result.
diff --git a/src/com/android/calculator2/KeyMaps.java b/src/com/android/calculator2/KeyMaps.java
index 48557c2..a7622cb 100644
--- a/src/com/android/calculator2/KeyMaps.java
+++ b/src/com/android/calculator2/KeyMaps.java
@@ -18,8 +18,14 @@
 
 import android.content.res.Resources;
 import android.content.Context;
+import android.app.Activity;
+import android.util.Log;
 import android.view.View;
+import android.widget.Button;
+
 import java.text.DecimalFormatSymbols;
+import java.util.HashMap;
+import java.util.Locale;
 
 public class KeyMaps {
     // Map key id to corresponding (internationalized) display string
@@ -145,19 +151,31 @@
         }
     }
 
-    final static char decimalPt =
+    static char decimalPt =
                 DecimalFormatSymbols.getInstance().getDecimalSeparator();
 
+    static char mPiChar;
+
+    static char mFactChar;
+
+    static HashMap<String, Integer> sKeyValForFun;
+        // Key value corresponding to given function name.
+        // We include both localized and English names.
+
+    static String sLocaleForFunMap = "none";
+        // Locale string corresponding to preceding ma and character
+        // constants.
+        // We recompute the map if this is not the current locale.
+
     // Return the button id corresponding to the supplied character
     // or NO_ID
-    // TODO: Should probably also check on characters used as button
-    // labels.  But those don't really seem to be internationalized.
-    public static int keyForChar(char c) {
+    // Called only by UI thread.
+    public static int keyForChar(char c, Activity a) {
+        validateFunMap(a);
         if (Character.isDigit(c)) {
             int i = Character.digit(c, 10);
             return KeyMaps.keyForDigVal(i);
         }
-        if (c == decimalPt) return R.id.dec_point;
         switch (c) {
         case '.':
             return R.id.dec_point;
@@ -169,12 +187,99 @@
             return R.id.op_mul;
         case '/':
             return R.id.op_div;
+        // TODO: We have an issue if any of the localized function
+        // names start with 'e' or 'p'.  That doesn't currently appear
+        // to be the case.  In fact the first letters of the Latin
+        // allphabet ones seem rather predictable.
         case 'e':
+        case 'E':
             return R.id.const_e;
+        case 'p':
+        case 'P':
+            return R.id.const_pi;
         case '^':
             return R.id.op_pow;
+        case '!':
+            return R.id.op_fact;
+        case '(':
+            return R.id.lparen;
+        case ')':
+            return R.id.rparen;
         default:
+            if (c == decimalPt) return R.id.dec_point;
+            if (c == mPiChar) return R.id.const_pi;
+                // pi is not translated, but it might be typable on
+                // a Greek keyboard, so we check ...
             return View.NO_ID;
         }
     }
+
+    // Add information corresponding to the given button id to
+    // sKeyValForFun.
+    static void addButton(int button_id, Activity a) {
+        Button button = (Button)a.findViewById(button_id);
+        sKeyValForFun.put(button.getText().toString(), button_id);
+    }
+
+    // Ensure that the preceding map and character constants are
+    // initialized and correspond to the current locale.
+    // Called only by a single thread, namely the UI thread.
+    static void validateFunMap(Activity a) {
+        Locale locale = Locale.getDefault();
+        String lname = locale.toString();
+        if (lname != sLocaleForFunMap) {
+            Log.v ("Calculator", "Setting local to: " + lname);
+            sKeyValForFun = new HashMap<String, Integer>();
+            sKeyValForFun.put("sin", R.id.fun_sin);
+            sKeyValForFun.put("cos", R.id.fun_cos);
+            sKeyValForFun.put("tan", R.id.fun_tan);
+            sKeyValForFun.put("arcsin", R.id.fun_arcsin);
+            sKeyValForFun.put("arccos", R.id.fun_arccos);
+            sKeyValForFun.put("arctan", R.id.fun_arctan);
+            sKeyValForFun.put("asin", R.id.fun_arcsin);
+            sKeyValForFun.put("acos", R.id.fun_arccos);
+            sKeyValForFun.put("atan", R.id.fun_arctan);
+            sKeyValForFun.put("ln", R.id.fun_ln);
+            sKeyValForFun.put("log", R.id.fun_log);
+            sKeyValForFun.put("sqrt", R.id.op_sqrt); // special treatment
+            addButton(R.id.fun_sin, a);
+            addButton(R.id.fun_cos, a);
+            addButton(R.id.fun_tan, a);
+            addButton(R.id.fun_arcsin, a);
+            addButton(R.id.fun_arccos, a);
+            addButton(R.id.fun_arctan, a);
+            addButton(R.id.fun_ln, a);
+            addButton(R.id.fun_log, a);
+
+            // Set locale-dependent character "constants"
+            decimalPt =
+                DecimalFormatSymbols.getInstance().getDecimalSeparator();
+            Resources res = a.getResources();
+            mPiChar = mFactChar = 0;
+            String piString = res.getString(R.string.const_pi);
+            if (piString.length() == 1) mPiChar = piString.charAt(0);
+            String factString = res.getString(R.string.op_fact);
+            if (factString.length() == 1) mFactChar = factString.charAt(0);
+
+            sLocaleForFunMap = lname;
+        }
+    }
+
+    // Return function button id for the substring of s starting
+    // at pos and ending with the next "(".
+    // Return NO_ID if there is none.
+    // We check for both standard English names and localized
+    // button labels, though those don't seem to differ much.
+    // Called only by a single thread, namely the UI thread.
+    public static int funForString(String s, int pos, Activity a) {
+        validateFunMap(a);
+        int parenPos = s.indexOf('(', pos);
+        if (parenPos != -1) {
+            String funString = s.substring(pos, parenPos);
+            Integer keyValue = sKeyValForFun.get(funString);
+            if (keyValue == null) return View.NO_ID;
+            return keyValue;
+        }
+        return View.NO_ID;
+    }
 }
diff --git a/tests/README.txt b/tests/README.txt
index 40b4f8a..3069de8 100644
--- a/tests/README.txt
+++ b/tests/README.txt
@@ -10,8 +10,8 @@
 
 1. A superficial test of calculator functionality through the UI.
 This is a resurrected version of a test that appeared in KitKat.
-It's currently mostly a placeholder and some basic infrastructure for
-future tests.
+This is currently only a placeholder for regression tests we shouldn't
+forget; it doesn't yet actually do much of anything.
 
 2. A test of the BoundedRationals library that mostly checks for agreement
 with the constructive reals (CR) package.  (The BoundedRationals package
diff --git a/tests/src/com/android/calculator2/BRTest.java b/tests/src/com/android/calculator2/BRTest.java
index bd7070a..1cd56a5 100644
--- a/tests/src/com/android/calculator2/BRTest.java
+++ b/tests/src/com/android/calculator2/BRTest.java
@@ -119,6 +119,9 @@
     }
 
     public void testBR() {
+        BoundedRational b = new BoundedRational(4,-6);
+        check(b.toString().equals("4/-6"), "toString(4/-6)");
+        check(b.toNiceString().equals("-2/3"),"toNiceString(4/-6)");
         checkEq(BR_0, CR.valueOf(0), "0");
         checkEq(BR_390, CR.valueOf(390), "390");
         checkEq(BR_15, CR.valueOf(15), "15");
diff --git a/tests/src/com/android/calculator2/CalculatorHitSomeButtons.java b/tests/src/com/android/calculator2/CalculatorHitSomeButtons.java
index d26a5cb..a075a64 100644
--- a/tests/src/com/android/calculator2/CalculatorHitSomeButtons.java
+++ b/tests/src/com/android/calculator2/CalculatorHitSomeButtons.java
@@ -26,9 +26,9 @@
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.View;
-import android.widget.EditText;
 import android.widget.Button;
 import android.widget.LinearLayout;
+import android.widget.TextView;
 import android.graphics.Rect;
 import android.test.TouchUtils;
 
@@ -64,7 +64,7 @@
         super.tearDown();
     }
 
-/*
+
     @LargeTest
     public void testPressSomeKeys() {
         Log.v(TAG, "Pressing some keys!");
@@ -83,7 +83,7 @@
 
         checkDisplay("23");
     }
-*/
+
 
     @LargeTest
     public void testTapSomeButtons() {
@@ -102,6 +102,7 @@
         tap(R.id.digit_7);
         tap(R.id.op_div);
         tap(R.id.digit_3);
+        tap(R.id.dec_point);
         tap(R.id.eq);
 
         checkDisplay("189");
@@ -116,6 +117,24 @@
 
         // Careful: the first digit in the expected value is \u2212, not "-" (a hyphen)
         checkDisplay(mActivity.getString(R.string.op_sub) + "600");
+
+        tap(R.id.dec_point);
+        tap(R.id.digit_5);
+        tap(R.id.op_add);
+        tap(R.id.dec_point);
+        tap(R.id.digit_5);
+        tap(R.id.eq);
+        checkDisplay("1");
+
+        tap(R.id.digit_5);
+        tap(R.id.op_div);
+        tap(R.id.digit_3);
+        tap(R.id.dec_point);
+        tap(R.id.digit_5);
+        tap(R.id.op_mul);
+        tap(R.id.digit_7);
+        tap(R.id.eq);
+        checkDisplay("10");
     }
 
     // helper functions
@@ -123,39 +142,35 @@
         mInst.sendKeyDownUpSync(keycode);
     }
 
-    private boolean tap(int id) {
+    private void tap(int id) {
         View view = mActivity.findViewById(id);
-        if(view != null) {
-            TouchUtils.clickView(this, view);
-            return true;
-        }
-        return false;
+        assertNotNull(view);
+        TouchUtils.clickView(this, view);
     }
 
     private void checkDisplay(final String s) {
-        mInst.waitForIdle(new Runnable () {
-            @Override
-            public void run() {
-                try {
-                    Thread.sleep(20); // Wait for background computation
-                } catch(InterruptedException ignored) {
-                    fail("Unexpected interrupt");
+    /*
+        FIXME: This doesn't yet work.
+        try {
+            Thread.sleep(20);
+            runTestOnUiThread(new Runnable () {
+                @Override
+                public void run() {
+                    Log.v(TAG, "Display:" + displayVal());
+                    assertEquals(s, displayVal());
                 }
-                mInst.waitForIdle(new Runnable () {
-                    @Override
-                    public void run() {
-                        assertEquals(displayVal(), s);
-                    }
-                });
-            }
-        });
+            });
+        } catch (Throwable e) {
+            fail("unexpected exception" + e);
+        }
+    */
     }
 
     private String displayVal() {
         CalculatorResult display = (CalculatorResult) mActivity.findViewById(R.id.result);
         assertNotNull(display);
 
-        EditText box = (EditText) display;
+        TextView box = (TextView) display;
         assertNotNull(box);
 
         return box.getText().toString();