Merge remote-tracking branch 'goog/ub-calculator-euler' am: e5459bb447
am: 0e0cf47268

Change-Id: I77b914a99b678518efe0f688fc78c64e318773e1
diff --git a/Android.mk b/Android.mk
index 39f475e..5b92157 100644
--- a/Android.mk
+++ b/Android.mk
@@ -17,19 +17,34 @@
 LOCAL_PATH := $(call my-dir)
 include $(CLEAR_VARS)
 
+LOCAL_RESOURCE_DIR := packages/apps/ExactCalculator/res
+
+ifeq ($(TARGET_BUILD_APPS),)
+LOCAL_RESOURCE_DIR += frameworks/support/v7/gridlayout/res
+LOCAL_RESOURCE_DIR += frameworks/support/v7/recyclerview/res
+else
+LOCAL_RESOURCE_DIR += prebuilts/sdk/current/support/v7/gridlayout/res
+LOCAL_RESOURCE_DIR += prebuilts/sdk/current/support/v7/recyclerview/res
+endif
+
 LOCAL_MODULE_TAGS := optional
-
-LOCAL_OVERRIDES_PACKAGES := Calculator
-LOCAL_PACKAGE_NAME := ExactCalculator
-
 LOCAL_SDK_VERSION := current
 
+LOCAL_PACKAGE_NAME := ExactCalculator
+LOCAL_OVERRIDES_PACKAGES := Calculator
+
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
-LOCAL_STATIC_JAVA_LIBRARIES := cr android-support-v4
-
 LOCAL_PROGUARD_FLAG_FILES := proguard.flags
+LOCAL_PROGUARD_FLAG_FILES += ../../../frameworks/support/v7/recyclerview/proguard-rules.pro
+
+LOCAL_STATIC_JAVA_LIBRARIES := cr
+LOCAL_STATIC_JAVA_LIBRARIES += android-support-v4
+LOCAL_STATIC_JAVA_LIBRARIES += android-support-v7-gridlayout
+LOCAL_STATIC_JAVA_LIBRARIES += android-support-v7-recyclerview
+
+LOCAL_AAPT_FLAGS := --auto-add-overlay
+LOCAL_AAPT_FLAGS += --extra-packages android.support.v7.gridlayout
+LOCAL_AAPT_FLAGS += --extra-packages android.support.v7.recyclerview
 
 include $(BUILD_PACKAGE)
-
-include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/res/drawable/ic_history_grey600_48dp.xml b/res/drawable/ic_history_grey600_48dp.xml
new file mode 100644
index 0000000..65103fc
--- /dev/null
+++ b/res/drawable/ic_history_grey600_48dp.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportHeight="48.0"
+    android:viewportWidth="48.0">
+    <path
+        android:fillColor="#757575"
+        android:pathData="M25.99,6C16.04,6 8,14.06 8,24H2l7.79,7.79 0.14,0.29L18,24h-6c0,-7.73
+        6.27,-14 14,-14s14,6.27 14,14 -6.27,14 -14,14c-3.87,0 -7.36,-1.58
+        -9.89,-4.11l-2.83,2.83C16.53,39.98 21.02,42 25.99,42 35.94,42 44,33.94 44,24S35.94,6
+        25.99,6zM24,16v10l8.56,5.08L34,28.65l-7,-4.15V16h-3z" />
+</vector>
\ No newline at end of file
diff --git a/res/layout/activity_calculator_land.xml b/res/layout/activity_calculator_land.xml
index 5dd2c20..a19cd86 100644
--- a/res/layout/activity_calculator_land.xml
+++ b/res/layout/activity_calculator_land.xml
@@ -37,4 +37,4 @@
 
     </LinearLayout>
 
-</LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/activity_calculator_main.xml b/res/layout/activity_calculator_main.xml
new file mode 100644
index 0000000..2e9d62d
--- /dev/null
+++ b/res/layout/activity_calculator_main.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<com.android.calculator2.DragLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/drag_layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <include
+        android:id="@+id/main_calculator"
+        layout="@layout/activity_calculator" />
+
+    <FrameLayout
+        android:id="@+id/history_frame"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:visibility="invisible" />
+
+</com.android.calculator2.DragLayout>
diff --git a/res/layout/activity_calculator_port.xml b/res/layout/activity_calculator_port.xml
index 4cafa94..30aaf00 100644
--- a/res/layout/activity_calculator_port.xml
+++ b/res/layout/activity_calculator_port.xml
@@ -21,7 +21,7 @@
     android:layout_height="match_parent"
     android:orientation="vertical">
 
-    <include layout="@layout/display"/>
+    <include layout="@layout/display" />
 
     <com.android.calculator2.CalculatorPadViewPager
         android:id="@+id/pad_pager"
diff --git a/res/layout/dialog_message.xml b/res/layout/dialog_message.xml
index 67e6089..233849e 100644
--- a/res/layout/dialog_message.xml
+++ b/res/layout/dialog_message.xml
@@ -15,13 +15,12 @@
   limitations under the License.
   -->
 
-<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+<TextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/message"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:paddingBottom="20dip"
-    android:paddingEnd="16dip"
-    android:paddingStart="20dip"
-    android:paddingTop="20dip"
+    android:padding="20dip"
+    android:ellipsize="none"
     android:textAppearance="?android:attr/textAppearanceMedium"
     android:textIsSelectable="true" />
diff --git a/res/layout/display_one_line.xml b/res/layout/display_one_line.xml
index c016b15..11760fb 100644
--- a/res/layout/display_one_line.xml
+++ b/res/layout/display_one_line.xml
@@ -39,7 +39,7 @@
             android:overScrollMode="never"
             android:scrollbars="none">
 
-            <com.android.calculator2.CalculatorText
+            <com.android.calculator2.CalculatorFormula
                 android:id="@+id/formula"
                 style="@style/DisplayTextStyle.Formula"
                 android:layout_width="wrap_content"
diff --git a/res/layout/display_two_line.xml b/res/layout/display_two_line.xml
index 3735a85..3f7338d 100644
--- a/res/layout/display_two_line.xml
+++ b/res/layout/display_two_line.xml
@@ -33,17 +33,14 @@
         android:overScrollMode="never"
         android:scrollbars="none">
 
-       <com.android.calculator2.CalculatorText
+       <com.android.calculator2.CalculatorFormula
             android:id="@+id/formula"
             style="@style/DisplayTextStyle.Formula"
             android:layout_width="wrap_content"
             android:layout_height="match_parent"
-            android:layout_gravity="bottom|end"
             android:ellipsize="none"
-            android:longClickable="true"
-            android:singleLine="true"
-            android:textColor="@color/display_formula_text_color"
-            android:textIsSelectable="false" />
+            android:maxLines="1"
+            android:textColor="@color/display_formula_text_color" />
 
     </com.android.calculator2.CalculatorScrollView>
 
diff --git a/res/layout/empty_history_view.xml b/res/layout/empty_history_view.xml
new file mode 100644
index 0000000..7814b52
--- /dev/null
+++ b/res/layout/empty_history_view.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/empty_history_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:drawableTop="@drawable/ic_history_grey600_48dp"
+        android:text="@string/no_history"
+        android:textSize="20sp" />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/res/layout/fragment_history.xml b/res/layout/fragment_history.xml
new file mode 100644
index 0000000..e5fe50e
--- /dev/null
+++ b/res/layout/fragment_history.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:clipChildren="false"
+    android:orientation="vertical">
+
+    <Toolbar
+        android:id="@+id/history_toolbar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:background="?android:attr/colorPrimary"
+        android:elevation="4dip"
+        android:minHeight="?android:attr/actionBarSize"
+        android:navigationContentDescription="@string/desc_navigate_up"
+        android:navigationIcon="?android:attr/homeAsUpIndicator"
+        android:popupTheme="@android:style/ThemeOverlay.Material.Light"
+        android:theme="@android:style/ThemeOverlay.Material.Dark.ActionBar"
+        android:title="@string/title_history" />
+
+    <android.support.v7.widget.RecyclerView
+        android:id="@+id/history_recycler_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@color/display_background_color"
+        android:clipChildren="false"
+        android:clipToPadding="false"
+        android:paddingBottom="@dimen/history_divider_padding"
+        android:visibility="invisible"
+        app:layoutManager="LinearLayoutManager"
+        app:reverseLayout="true" />
+
+</LinearLayout>
diff --git a/res/layout/history_item.xml b/res/layout/history_item.xml
new file mode 100644
index 0000000..cf8b6c0
--- /dev/null
+++ b/res/layout/history_item.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingTop="@dimen/history_divider_padding"
+    android:clipChildren="false"
+    android:clipToPadding="false"
+    android:importantForAccessibility="no"
+    android:orientation="vertical">
+
+    <View
+        android:id="@+id/history_divider"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/history_divider_padding"
+        android:layout_marginBottom="@dimen/history_divider_padding"
+        android:background="?android:attr/listDivider"
+        android:importantForAccessibility="no" />
+
+    <TextView
+        android:id="@+id/history_date"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:fontFamily="sans-serif-medium"
+        android:paddingStart="@dimen/result_padding_start"
+        android:paddingEnd="@dimen/result_padding_end"
+        android:text="@string/title_current_expression"
+        android:textColor="?android:attr/colorAccent"
+        android:textSize="14dp" />
+
+    <com.android.calculator2.CalculatorScrollView
+        android:id="@+id/history_formula_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:overScrollMode="never"
+        android:scrollbars="none">
+
+        <com.android.calculator2.AlignedTextView
+            android:id="@+id/history_formula"
+            style="@style/HistoryItemTextStyle"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:ellipsize="none"
+            android:textColor="@color/display_formula_text_color" />
+
+    </com.android.calculator2.CalculatorScrollView>
+
+    <com.android.calculator2.CalculatorResult
+        android:id="@+id/history_result"
+        style="@style/HistoryItemTextStyle"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:bufferType="spannable"
+        android:maxLines="1"
+        android:textColor="@color/display_result_text_color" />
+
+</LinearLayout>
diff --git a/res/layout/pad_advanced_3x5.xml b/res/layout/pad_advanced_3x5.xml
index e303133..ece7003 100644
--- a/res/layout/pad_advanced_3x5.xml
+++ b/res/layout/pad_advanced_3x5.xml
@@ -15,193 +15,194 @@
   limitations under the License.
   -->
 
-<GridLayout
+<android.support.v7.widget.GridLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:id="@+id/pad_advanced"
     style="@style/PadLayoutStyle.Advanced"
-    android:rowCount="5"
-    android:columnCount="3"
-    android:background="@color/pad_advanced_background_color">
+    android:background="@color/pad_advanced_background_color"
+    app:rowCount="5"
+    app:columnCount="3">
 
     <Button
         android:id="@+id/toggle_inv"
         style="@style/PadButtonStyle.Advanced.Text"
-        android:layout_row="0"
-        android:layout_column="0"
         android:background="@drawable/pad_button_inverse_background"
         android:contentDescription="@string/desc_inv_off"
-        android:text="@string/inv" />
+        android:text="@string/inv"
+        app:layout_row="0"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/toggle_mode"
         style="@style/PadButtonStyle.Advanced.Text"
-        android:layout_row="0"
-        android:layout_column="1"
         android:contentDescription="@string/desc_switch_deg"
-        android:text="@string/mode_deg" />
+        android:text="@string/mode_deg"
+        app:layout_row="0"
+        app:layout_column="1" />
 
     <Button
         android:id="@+id/op_pct"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="0"
-        android:layout_column="2"
         android:contentDescription="@string/desc_op_pct"
-        android:text="@string/op_pct" />
+        android:text="@string/op_pct"
+        app:layout_row="0"
+        app:layout_column="2" />
 
     <Button
         android:id="@+id/fun_sin"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="1"
-        android:layout_column="0"
         android:contentDescription="@string/desc_fun_sin"
-        android:text="@string/fun_sin" />
+        android:text="@string/fun_sin"
+        app:layout_row="1"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/fun_arcsin"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="1"
-        android:layout_column="0"
         android:contentDescription="@string/desc_fun_arcsin"
         android:fontFamily="sans-serif-medium"
         android:text="@string/fun_arcsin"
-        android:visibility="gone" />
+        android:visibility="gone"
+        app:layout_row="1"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/fun_cos"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="1"
-        android:layout_column="1"
         android:contentDescription="@string/desc_fun_cos"
-        android:text="@string/fun_cos" />
+        android:text="@string/fun_cos"
+        app:layout_row="1"
+        app:layout_column="1" />
 
     <Button
         android:id="@+id/fun_arccos"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="1"
-        android:layout_column="1"
         android:contentDescription="@string/desc_fun_arccos"
         android:fontFamily="sans-serif-medium"
         android:text="@string/fun_arccos"
-        android:visibility="gone" />
+        android:visibility="gone"
+        app:layout_row="1"
+        app:layout_column="1" />
 
     <Button
         android:id="@+id/fun_tan"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="1"
-        android:layout_column="2"
         android:contentDescription="@string/desc_fun_tan"
-        android:text="@string/fun_tan" />
+        android:text="@string/fun_tan"
+        app:layout_row="1"
+        app:layout_column="2" />
 
     <Button
         android:id="@+id/fun_arctan"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="1"
-        android:layout_column="2"
         android:contentDescription="@string/desc_fun_arctan"
         android:fontFamily="sans-serif-medium"
         android:text="@string/fun_arctan"
-        android:visibility="gone" />
+        android:visibility="gone"
+        app:layout_row="1"
+        app:layout_column="2" />
 
     <Button
         android:id="@+id/fun_ln"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="2"
-        android:layout_column="0"
         android:contentDescription="@string/desc_fun_ln"
-        android:text="@string/fun_ln" />
+        android:text="@string/fun_ln"
+        app:layout_row="2"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/fun_exp"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="2"
-        android:layout_column="0"
         android:contentDescription="@string/desc_fun_exp"
         android:fontFamily="sans-serif-medium"
         android:text="@string/fun_exp"
-        android:visibility="gone" />
+        android:visibility="gone"
+        app:layout_row="2"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/fun_log"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="2"
-        android:layout_column="1"
         android:contentDescription="@string/desc_fun_log"
-        android:text="@string/fun_log" />
+        android:text="@string/fun_log"
+        app:layout_row="2"
+        app:layout_column="1" />
 
     <Button
         android:id="@+id/fun_10pow"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="2"
-        android:layout_column="1"
         android:contentDescription="@string/desc_fun_10pow"
         android:fontFamily="sans-serif-medium"
         android:text="@string/fun_10pow"
-        android:visibility="gone" />
+        android:visibility="gone"
+        app:layout_row="2"
+        app:layout_column="1" />
 
     <Button
         android:id="@+id/op_fact"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="2"
-        android:layout_column="2"
         android:contentDescription="@string/desc_op_fact"
-        android:text="@string/op_fact" />
+        android:text="@string/op_fact"
+        app:layout_row="2"
+        app:layout_column="2" />
 
     <Button
         android:id="@+id/const_pi"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="3"
-        android:layout_column="0"
         android:contentDescription="@string/desc_const_pi"
-        android:text="@string/const_pi" />
+        android:text="@string/const_pi"
+        app:layout_row="3"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/const_e"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="3"
-        android:layout_column="1"
         android:contentDescription="@string/desc_const_e"
-        android:text="@string/const_e" />
+        android:text="@string/const_e"
+        app:layout_row="3"
+        app:layout_column="1" />
 
     <Button
         android:id="@+id/op_pow"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="3"
-        android:layout_column="2"
         android:contentDescription="@string/desc_op_pow"
-        android:text="@string/op_pow" />
+        android:text="@string/op_pow"
+        app:layout_row="3"
+        app:layout_column="2" />
 
     <Button
         android:id="@+id/lparen"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="4"
-        android:layout_column="0"
         android:contentDescription="@string/desc_lparen"
-        android:text="@string/lparen" />
+        android:text="@string/lparen"
+        app:layout_row="4"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/rparen"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="4"
-        android:layout_column="1"
         android:contentDescription="@string/desc_rparen"
-        android:text="@string/rparen" />
+        android:text="@string/rparen"
+        app:layout_row="4"
+        app:layout_column="1" />
 
     <Button
         android:id="@+id/op_sqrt"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="4"
-        android:layout_column="2"
         android:contentDescription="@string/desc_op_sqrt"
-        android:text="@string/op_sqrt" />
+        android:text="@string/op_sqrt"
+        app:layout_row="4"
+        app:layout_column="2" />
 
     <Button
         android:id="@+id/op_sqr"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="4"
-        android:layout_column="2"
         android:contentDescription="@string/desc_op_sqr"
         android:fontFamily="sans-serif-medium"
         android:text="@string/op_sqr"
-        android:visibility="gone" />
+        android:visibility="gone"
+        app:layout_row="4"
+        app:layout_column="2" />
 
-</GridLayout>
+</android.support.v7.widget.GridLayout>
diff --git a/res/layout/pad_advanced_4x4.xml b/res/layout/pad_advanced_4x4.xml
index ddbee47..70a520e 100644
--- a/res/layout/pad_advanced_4x4.xml
+++ b/res/layout/pad_advanced_4x4.xml
@@ -15,193 +15,194 @@
   limitations under the License.
   -->
 
-<GridLayout
+<android.support.v7.widget.GridLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:id="@+id/pad_advanced"
     style="@style/PadLayoutStyle.Advanced"
-    android:rowCount="4"
-    android:columnCount="4"
-    android:background="@color/pad_advanced_background_color">
+    android:background="@color/pad_advanced_background_color"
+    app:rowCount="4"
+    app:columnCount="4">
 
     <Button
         android:id="@+id/toggle_inv"
         style="@style/PadButtonStyle.Advanced.Text"
-        android:layout_row="0"
-        android:layout_column="0"
         android:background="@drawable/pad_button_inverse_background"
         android:contentDescription="@string/desc_inv_off"
-        android:text="@string/inv" />
+        android:text="@string/inv"
+        app:layout_row="0"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/toggle_mode"
         style="@style/PadButtonStyle.Advanced.Text"
-        android:layout_row="0"
-        android:layout_column="1"
         android:contentDescription="@string/desc_switch_deg"
-        android:text="@string/mode_deg" />
+        android:text="@string/mode_deg"
+        app:layout_row="0"
+        app:layout_column="1" />
 
     <Button
         android:id="@+id/op_pct"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="0"
-        android:layout_column="2"
         android:contentDescription="@string/desc_op_pct"
-        android:text="@string/op_pct" />
+        android:text="@string/op_pct"
+        app:layout_row="0"
+        app:layout_column="2" />
 
     <Button
         android:id="@+id/fun_sin"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="1"
-        android:layout_column="0"
         android:contentDescription="@string/desc_fun_sin"
-        android:text="@string/fun_sin" />
+        android:text="@string/fun_sin"
+        app:layout_row="1"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/fun_arcsin"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="1"
-        android:layout_column="0"
         android:contentDescription="@string/desc_fun_arcsin"
         android:fontFamily="sans-serif-medium"
         android:text="@string/fun_arcsin"
-        android:visibility="gone" />
+        android:visibility="gone"
+        app:layout_row="1"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/fun_cos"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="1"
-        android:layout_column="1"
         android:contentDescription="@string/desc_fun_cos"
-        android:text="@string/fun_cos" />
+        android:text="@string/fun_cos"
+        app:layout_row="1"
+        app:layout_column="1" />
 
     <Button
         android:id="@+id/fun_arccos"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="1"
-        android:layout_column="1"
         android:contentDescription="@string/desc_fun_arccos"
         android:fontFamily="sans-serif-medium"
         android:text="@string/fun_arccos"
-        android:visibility="gone" />
+        android:visibility="gone"
+        app:layout_row="1"
+        app:layout_column="1" />
 
     <Button
         android:id="@+id/fun_tan"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="1"
-        android:layout_column="2"
         android:contentDescription="@string/desc_fun_tan"
-        android:text="@string/fun_tan" />
+        android:text="@string/fun_tan"
+        app:layout_row="1"
+        app:layout_column="2" />
 
     <Button
         android:id="@+id/fun_arctan"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="1"
-        android:layout_column="2"
         android:contentDescription="@string/desc_fun_arctan"
         android:fontFamily="sans-serif-medium"
         android:text="@string/fun_arctan"
-        android:visibility="gone" />
+        android:visibility="gone"
+        app:layout_row="1"
+        app:layout_column="2" />
 
     <Button
         android:id="@+id/const_pi"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="1"
-        android:layout_column="3"
         android:contentDescription="@string/desc_const_pi"
-        android:text="@string/const_pi" />
+        android:text="@string/const_pi"
+        app:layout_row="1"
+        app:layout_column="3" />
 
     <Button
         android:id="@+id/fun_ln"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="2"
-        android:layout_column="0"
         android:contentDescription="@string/desc_fun_ln"
-        android:text="@string/fun_ln" />
+        android:text="@string/fun_ln"
+        app:layout_row="2"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/fun_exp"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="2"
-        android:layout_column="0"
         android:contentDescription="@string/desc_fun_exp"
         android:fontFamily="sans-serif-medium"
         android:text="@string/fun_exp"
-        android:visibility="gone" />
+        android:visibility="gone"
+        app:layout_row="2"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/fun_log"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="2"
-        android:layout_column="1"
         android:contentDescription="@string/desc_fun_log"
-        android:text="@string/fun_log" />
+        android:text="@string/fun_log"
+        app:layout_row="2"
+        app:layout_column="1" />
 
     <Button
         android:id="@+id/fun_10pow"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="2"
-        android:layout_column="1"
         android:contentDescription="@string/desc_fun_10pow"
         android:fontFamily="sans-serif-medium"
         android:text="@string/fun_10pow"
-        android:visibility="gone" />
+        android:visibility="gone"
+        app:layout_row="2"
+        app:layout_column="1" />
 
     <Button
         android:id="@+id/op_fact"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="2"
-        android:layout_column="2"
         android:contentDescription="@string/desc_op_fact"
-        android:text="@string/op_fact" />
+        android:text="@string/op_fact"
+        app:layout_row="2"
+        app:layout_column="2" />
 
     <Button
         android:id="@+id/const_e"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="2"
-        android:layout_column="3"
         android:contentDescription="@string/desc_const_e"
-        android:text="@string/const_e" />
+        android:text="@string/const_e"
+        app:layout_row="2"
+        app:layout_column="3" />
 
     <Button
         android:id="@+id/lparen"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="3"
-        android:layout_column="0"
         android:contentDescription="@string/desc_lparen"
-        android:text="@string/lparen" />
+        android:text="@string/lparen"
+        app:layout_row="3"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/rparen"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="3"
-        android:layout_column="1"
         android:contentDescription="@string/desc_rparen"
-        android:text="@string/rparen" />
+        android:text="@string/rparen"
+        app:layout_row="3"
+        app:layout_column="1" />
 
     <Button
         android:id="@+id/op_sqrt"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="3"
-        android:layout_column="2"
         android:contentDescription="@string/desc_op_sqrt"
-        android:text="@string/op_sqrt" />
+        android:text="@string/op_sqrt"
+        app:layout_row="3"
+        app:layout_column="2" />
 
     <Button
         android:id="@+id/op_sqr"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="3"
-        android:layout_column="2"
         android:contentDescription="@string/desc_op_sqr"
         android:fontFamily="sans-serif-medium"
         android:text="@string/op_sqr"
-        android:visibility="gone" />
+        android:visibility="gone"
+        app:layout_row="3"
+        app:layout_column="2" />
 
     <Button
         android:id="@+id/op_pow"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="3"
-        android:layout_column="3"
         android:contentDescription="@string/desc_op_pow"
-        android:text="@string/op_pow" />
+        android:text="@string/op_pow"
+        app:layout_row="3"
+        app:layout_column="3" />
 
-</GridLayout>
\ No newline at end of file
+</android.support.v7.widget.GridLayout>
diff --git a/res/layout/pad_advanced_5x3.xml b/res/layout/pad_advanced_5x3.xml
index b8de7fd..95a0fc1 100644
--- a/res/layout/pad_advanced_5x3.xml
+++ b/res/layout/pad_advanced_5x3.xml
@@ -15,193 +15,194 @@
   limitations under the License.
   -->
 
-<GridLayout
+<android.support.v7.widget.GridLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:id="@+id/pad_advanced"
     style="@style/PadLayoutStyle.Advanced"
-    android:rowCount="3"
-    android:columnCount="5"
-    android:background="@color/pad_advanced_background_color">
+    android:background="@color/pad_advanced_background_color"
+    app:rowCount="3"
+    app:columnCount="5">
 
     <Button
         android:id="@+id/toggle_inv"
         style="@style/PadButtonStyle.Advanced.Text"
-        android:layout_row="0"
-        android:layout_column="0"
         android:background="@drawable/pad_button_inverse_background"
         android:contentDescription="@string/desc_inv_off"
-        android:text="@string/inv" />
+        android:text="@string/inv"
+        app:layout_row="0"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/toggle_mode"
         style="@style/PadButtonStyle.Advanced.Text"
-        android:layout_row="0"
-        android:layout_column="1"
         android:contentDescription="@string/desc_switch_deg"
-        android:text="@string/mode_deg" />
+        android:text="@string/mode_deg"
+        app:layout_row="0"
+        app:layout_column="1" />
 
     <Button
         android:id="@+id/fun_sin"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="0"
-        android:layout_column="2"
         android:contentDescription="@string/desc_fun_sin"
-        android:text="@string/fun_sin" />
+        android:text="@string/fun_sin"
+        app:layout_row="0"
+        app:layout_column="2" />
 
     <Button
         android:id="@+id/fun_arcsin"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="0"
-        android:layout_column="2"
         android:contentDescription="@string/desc_fun_arcsin"
         android:fontFamily="sans-serif-medium"
         android:text="@string/fun_arcsin"
-        android:visibility="gone" />
+        android:visibility="gone"
+        app:layout_row="0"
+        app:layout_column="2" />
 
     <Button
         android:id="@+id/fun_cos"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="0"
-        android:layout_column="3"
         android:contentDescription="@string/desc_fun_cos"
-        android:text="@string/fun_cos" />
+        android:text="@string/fun_cos"
+        app:layout_row="0"
+        app:layout_column="3" />
 
     <Button
         android:id="@+id/fun_arccos"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="0"
-        android:layout_column="3"
         android:contentDescription="@string/desc_fun_arccos"
         android:fontFamily="sans-serif-medium"
         android:text="@string/fun_arccos"
-        android:visibility="gone" />
+        android:visibility="gone"
+        app:layout_row="0"
+        app:layout_column="3" />
 
     <Button
         android:id="@+id/fun_tan"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="0"
-        android:layout_column="4"
         android:contentDescription="@string/desc_fun_tan"
-        android:text="@string/fun_tan" />
+        android:text="@string/fun_tan"
+        app:layout_row="0"
+        app:layout_column="4" />
 
     <Button
         android:id="@+id/fun_arctan"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="0"
-        android:layout_column="4"
         android:contentDescription="@string/desc_fun_arctan"
         android:fontFamily="sans-serif-medium"
         android:text="@string/fun_arctan"
-        android:visibility="gone" />
+        android:visibility="gone"
+        app:layout_row="0"
+        app:layout_column="4" />
 
     <Button
         android:id="@+id/op_pct"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="1"
-        android:layout_column="0"
         android:contentDescription="@string/desc_op_pct"
-        android:text="@string/op_pct" />
+        android:text="@string/op_pct"
+        app:layout_row="1"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/fun_ln"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="1"
-        android:layout_column="1"
         android:contentDescription="@string/desc_fun_ln"
-        android:text="@string/fun_ln" />
+        android:text="@string/fun_ln"
+        app:layout_row="1"
+        app:layout_column="1" />
 
     <Button
         android:id="@+id/fun_exp"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="1"
-        android:layout_column="1"
         android:contentDescription="@string/desc_fun_exp"
         android:fontFamily="sans-serif-medium"
         android:text="@string/fun_exp"
-        android:visibility="gone" />
+        android:visibility="gone"
+        app:layout_row="1"
+        app:layout_column="1" />
 
     <Button
         android:id="@+id/fun_log"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="1"
-        android:layout_column="2"
         android:contentDescription="@string/desc_fun_log"
-        android:text="@string/fun_log" />
+        android:text="@string/fun_log"
+        app:layout_row="1"
+        app:layout_column="2" />
 
     <Button
         android:id="@+id/fun_10pow"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="1"
-        android:layout_column="2"
         android:contentDescription="@string/desc_fun_10pow"
         android:fontFamily="sans-serif-medium"
         android:text="@string/fun_10pow"
-        android:visibility="gone" />
+        android:visibility="gone"
+        app:layout_row="1"
+        app:layout_column="2" />
 
     <Button
         android:id="@+id/op_fact"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="1"
-        android:layout_column="3"
         android:contentDescription="@string/desc_op_fact"
-        android:text="@string/op_fact" />
+        android:text="@string/op_fact"
+        app:layout_row="1"
+        app:layout_column="3" />
 
     <Button
         android:id="@+id/op_pow"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="1"
-        android:layout_column="4"
         android:contentDescription="@string/desc_op_pow"
-        android:text="@string/op_pow" />
+        android:text="@string/op_pow"
+        app:layout_row="1"
+        app:layout_column="4" />
 
     <Button
         android:id="@+id/const_pi"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="2"
-        android:layout_column="0"
         android:contentDescription="@string/desc_const_pi"
-        android:text="@string/const_pi" />
+        android:text="@string/const_pi"
+        app:layout_row="2"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/const_e"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="2"
-        android:layout_column="1"
         android:contentDescription="@string/desc_const_e"
-        android:text="@string/const_e" />
+        android:text="@string/const_e"
+        app:layout_row="2"
+        app:layout_column="1" />
 
     <Button
         android:id="@+id/lparen"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="2"
-        android:layout_column="2"
         android:contentDescription="@string/desc_lparen"
-        android:text="@string/lparen" />
+        android:text="@string/lparen"
+        app:layout_row="2"
+        app:layout_column="2" />
 
     <Button
         android:id="@+id/rparen"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="2"
-        android:layout_column="3"
         android:contentDescription="@string/desc_rparen"
-        android:text="@string/rparen" />
+        android:text="@string/rparen"
+        app:layout_row="2"
+        app:layout_column="3" />
 
     <Button
         android:id="@+id/op_sqrt"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="2"
-        android:layout_column="4"
         android:contentDescription="@string/desc_op_sqrt"
-        android:text="@string/op_sqrt" />
+        android:text="@string/op_sqrt"
+        app:layout_row="2"
+        app:layout_column="4" />
 
     <Button
         android:id="@+id/op_sqr"
         style="@style/PadButtonStyle.Advanced"
-        android:layout_row="2"
-        android:layout_column="4"
         android:contentDescription="@string/desc_op_sqr"
         android:fontFamily="sans-serif-medium"
         android:text="@string/op_sqr"
-        android:visibility="gone" />
+        android:visibility="gone"
+        app:layout_row="2"
+        app:layout_column="4" />
 
-</GridLayout>
+</android.support.v7.widget.GridLayout>
diff --git a/res/layout/pad_numeric.xml b/res/layout/pad_numeric.xml
index 34e9dbc..2f301e8 100644
--- a/res/layout/pad_numeric.xml
+++ b/res/layout/pad_numeric.xml
@@ -15,98 +15,98 @@
   limitations under the License.
   -->
 
-<GridLayout
+<android.support.v7.widget.GridLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:id="@+id/pad_numeric"
     style="@style/PadLayoutStyle.Numeric"
-    android:rowCount="4"
-    android:columnCount="3"
-    android:background="@color/pad_numeric_background_color">
+    android:background="@color/pad_numeric_background_color"
+    app:rowCount="4"
+    app:columnCount="3">
 
     <Button
         android:id="@+id/digit_7"
         style="@style/PadButtonStyle.Numeric"
-        android:layout_row="0"
-        android:layout_column="0"
-        android:text="@string/digit_7" />
+        android:text="@string/digit_7"
+        app:layout_row="0"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/digit_8"
         style="@style/PadButtonStyle.Numeric"
-        android:layout_row="0"
-        android:layout_column="1"
-        android:text="@string/digit_8" />
+        android:text="@string/digit_8"
+        app:layout_row="0"
+        app:layout_column="1" />
 
     <Button
         android:id="@+id/digit_9"
         style="@style/PadButtonStyle.Numeric"
-        android:layout_row="0"
-        android:layout_column="2"
-        android:text="@string/digit_9" />
+        android:text="@string/digit_9"
+        app:layout_row="0"
+        app:layout_column="2" />
 
     <Button
         android:id="@+id/digit_4"
         style="@style/PadButtonStyle.Numeric"
-        android:layout_row="1"
-        android:layout_column="0"
-        android:text="@string/digit_4" />
+        android:text="@string/digit_4"
+        app:layout_row="1"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/digit_5"
         style="@style/PadButtonStyle.Numeric"
-        android:layout_row="1"
-        android:layout_column="1"
-        android:text="@string/digit_5" />
+        android:text="@string/digit_5"
+        app:layout_row="1"
+        app:layout_column="1" />
 
     <Button
         android:id="@+id/digit_6"
         style="@style/PadButtonStyle.Numeric"
-        android:layout_row="1"
-        android:layout_column="2"
-        android:text="@string/digit_6" />
+        android:text="@string/digit_6"
+        app:layout_row="1"
+        app:layout_column="2" />
 
     <Button
         android:id="@+id/digit_1"
         style="@style/PadButtonStyle.Numeric"
-        android:layout_row="2"
-        android:layout_column="0"
-        android:text="@string/digit_1" />
+        android:text="@string/digit_1"
+        app:layout_row="2"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/digit_2"
         style="@style/PadButtonStyle.Numeric"
-        android:layout_row="2"
-        android:layout_column="1"
-        android:text="@string/digit_2" />
+        android:text="@string/digit_2"
+        app:layout_row="2"
+        app:layout_column="1" />
 
     <Button
         android:id="@+id/digit_3"
         style="@style/PadButtonStyle.Numeric"
-        android:layout_row="2"
-        android:layout_column="2"
-        android:text="@string/digit_3" />
+        android:text="@string/digit_3"
+        app:layout_row="2"
+        app:layout_column="2" />
 
     <Button
         android:id="@+id/dec_point"
         style="@style/PadButtonStyle.Numeric"
-        android:layout_row="3"
-        android:layout_column="0"
         android:contentDescription="@string/desc_dec_point"
-        android:text="@string/dec_point" />
+        app:layout_row="3"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/digit_0"
         style="@style/PadButtonStyle.Numeric"
-        android:layout_row="3"
-        android:layout_column="1"
-        android:text="@string/digit_0" />
+        android:text="@string/digit_0"
+        app:layout_row="3"
+        app:layout_column="1" />
 
     <Button
         android:id="@+id/eq"
         style="@style/PadButtonStyle.Numeric.Equals"
-        android:layout_row="3"
-        android:layout_column="2"
         android:contentDescription="@string/desc_eq"
-        android:text="@string/eq" />
+        android:text="@string/eq"
+        app:layout_row="3"
+        app:layout_column="2" />
 
-</GridLayout>
+</android.support.v7.widget.GridLayout>
diff --git a/res/layout/pad_operator_one_col.xml b/res/layout/pad_operator_one_col.xml
index 1323b2c..3a6473a 100644
--- a/res/layout/pad_operator_one_col.xml
+++ b/res/layout/pad_operator_one_col.xml
@@ -15,62 +15,63 @@
   limitations under the License.
   -->
 
-<GridLayout
+<android.support.v7.widget.GridLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:id="@+id/pad_operator"
     style="@style/PadLayoutStyle.Operator"
-    android:rowCount="5"
-    android:columnCount="1"
-    android:background="@color/pad_operator_background_color">
+    android:background="@color/pad_operator_background_color"
+    app:rowCount="5"
+    app:columnCount="1">
 
     <Button
         android:id="@+id/del"
         style="@style/PadButtonStyle.Operator.Text"
-        android:layout_row="0"
-        android:layout_column="0"
         android:contentDescription="@string/desc_del"
-        android:text="@string/del" />
+        android:text="@string/del"
+        app:layout_row="0"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/clr"
         style="@style/PadButtonStyle.Operator.Text"
-        android:layout_row="0"
-        android:layout_column="0"
         android:contentDescription="@string/desc_clr"
         android:text="@string/clr"
-        android:visibility="gone" />
+        android:visibility="gone"
+        app:layout_row="0"
+        app:layout_column="0" />
 
 
     <Button
         android:id="@+id/op_div"
         style="@style/PadButtonStyle.Operator"
-        android:layout_row="1"
-        android:layout_column="0"
         android:contentDescription="@string/desc_op_div"
-        android:text="@string/op_div" />
+        android:text="@string/op_div"
+        app:layout_row="1"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/op_mul"
         style="@style/PadButtonStyle.Operator"
-        android:layout_row="2"
-        android:layout_column="0"
         android:contentDescription="@string/desc_op_mul"
-        android:text="@string/op_mul" />
+        android:text="@string/op_mul"
+        app:layout_row="2"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/op_sub"
         style="@style/PadButtonStyle.Operator"
-        android:layout_row="3"
-        android:layout_column="0"
         android:contentDescription="@string/desc_op_sub"
-        android:text="@string/op_sub" />
+        android:text="@string/op_sub"
+        app:layout_row="3"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/op_add"
         style="@style/PadButtonStyle.Operator"
-        android:layout_row="4"
-        android:layout_column="0"
         android:contentDescription="@string/desc_op_add"
-        android:text="@string/op_add" />
+        android:text="@string/op_add"
+        app:layout_row="4"
+        app:layout_column="0" />
 
-</GridLayout>
+</android.support.v7.widget.GridLayout>
diff --git a/res/layout/pad_operator_two_col.xml b/res/layout/pad_operator_two_col.xml
index 0d559d1..e056ba5 100644
--- a/res/layout/pad_operator_two_col.xml
+++ b/res/layout/pad_operator_two_col.xml
@@ -15,69 +15,70 @@
   limitations under the License.
   -->
 
-<GridLayout
+<android.support.v7.widget.GridLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:id="@+id/pad_operator"
     style="@style/PadLayoutStyle.Operator"
-    android:rowCount="4"
-    android:columnCount="2"
-    android:background="@color/pad_operator_background_color">
+    android:background="@color/pad_operator_background_color"
+    app:rowCount="4"
+    app:columnCount="2">
 
     <Button
         android:id="@+id/op_div"
         style="@style/PadButtonStyle.Operator"
-        android:layout_row="0"
-        android:layout_column="0"
         android:contentDescription="@string/desc_op_div"
-        android:text="@string/op_div" />
+        android:text="@string/op_div"
+        app:layout_row="0"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/del"
         style="@style/PadButtonStyle.Operator.Text"
-        android:layout_row="0"
-        android:layout_column="1"
         android:contentDescription="@string/desc_del"
-        android:text="@string/del" />
+        android:text="@string/del"
+        app:layout_row="0"
+        app:layout_column="1" />
 
     <Button
         android:id="@+id/clr"
         style="@style/PadButtonStyle.Operator.Text"
-        android:layout_row="0"
-        android:layout_column="1"
         android:contentDescription="@string/desc_clr"
         android:text="@string/clr"
-        android:visibility="gone" />
+        android:visibility="gone"
+        app:layout_row="0"
+        app:layout_column="1" />
 
     <Button
         android:id="@+id/op_mul"
         style="@style/PadButtonStyle.Operator"
-        android:layout_row="1"
-        android:layout_column="0"
         android:contentDescription="@string/op_mul"
-        android:text="@string/op_mul" />
+        android:text="@string/op_mul"
+        app:layout_row="1"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/op_sub"
         style="@style/PadButtonStyle.Operator"
-        android:layout_row="2"
-        android:layout_column="0"
         android:contentDescription="@string/desc_op_sub"
-        android:text="@string/op_sub" />
+        android:text="@string/op_sub"
+        app:layout_row="2"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/op_add"
         style="@style/PadButtonStyle.Operator"
-        android:layout_row="3"
-        android:layout_column="0"
         android:contentDescription="@string/desc_op_add"
-        android:text="@string/op_add" />
+        android:text="@string/op_add"
+        app:layout_row="3"
+        app:layout_column="0" />
 
     <Button
         android:id="@+id/eq"
         style="@style/PadButtonStyle.Operator"
-        android:layout_row="3"
-        android:layout_column="1"
         android:contentDescription="@string/desc_eq"
-        android:text="@string/eq" />
+        android:text="@string/eq"
+        app:layout_row="3"
+        app:layout_column="1" />
 
-</GridLayout>
+</android.support.v7.widget.GridLayout>
diff --git a/res/menu/activity_calculator.xml b/res/menu/activity_calculator.xml
index 8d086d1..d0c5659 100644
--- a/res/menu/activity_calculator.xml
+++ b/res/menu/activity_calculator.xml
@@ -17,6 +17,10 @@
 
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
 
+    <item
+        android:id="@+id/menu_history"
+        android:title="@string/menu_history" />
+
     <item android:id="@+id/menu_leading"
         android:title="@string/menu_leading" />
 
diff --git a/res/menu/copy.xml b/res/menu/copy.xml
deleted file mode 100644
index 5897f88..0000000
--- a/res/menu/copy.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
- * Copyright (C) 2011, 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.
- */
--->
-
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <item android:id="@+id/menu_copy"
-        android:title="@android:string/copy"/>
-
-</menu>
diff --git a/res/menu/fragment_history.xml b/res/menu/fragment_history.xml
new file mode 100644
index 0000000..8c9f1d6
--- /dev/null
+++ b/res/menu/fragment_history.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item
+        android:id="@+id/menu_clear_history"
+        android:title="@string/menu_clear_history" />
+
+</menu>
\ No newline at end of file
diff --git a/res/menu/paste.xml b/res/menu/menu_formula.xml
similarity index 84%
rename from res/menu/paste.xml
rename to res/menu/menu_formula.xml
index 964be0d..8882c8a 100644
--- a/res/menu/paste.xml
+++ b/res/menu/menu_formula.xml
@@ -19,7 +19,10 @@
 
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
 
+    <item android:id="@+id/memory_recall"
+          android:title="@string/memory_recall"/>
+
     <item android:id="@+id/menu_paste"
-        android:title="@android:string/paste"/>
+          android:title="@android:string/paste"/>
 
 </menu>
diff --git a/res/menu/paste.xml b/res/menu/menu_result.xml
similarity index 65%
copy from res/menu/paste.xml
copy to res/menu/menu_result.xml
index 964be0d..15e76cd 100644
--- a/res/menu/paste.xml
+++ b/res/menu/menu_result.xml
@@ -19,7 +19,20 @@
 
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <item android:id="@+id/menu_paste"
-        android:title="@android:string/paste"/>
+    <item
+        android:id="@+id/memory_store"
+        android:title="@string/memory_store" />
 
-</menu>
+    <item
+        android:id="@+id/memory_add"
+        android:title="@string/memory_add" />
+
+    <item
+        android:id="@+id/memory_subtract"
+        android:title="@string/memory_subtract" />
+
+    <item
+        android:id="@+id/menu_copy"
+        android:title="@android:string/copy" />
+
+</menu>
\ No newline at end of file
diff --git a/res/values-w230dp-h220dp/styles.xml b/res/values-w230dp-h220dp/styles.xml
deleted file mode 100644
index 88a52ce..0000000
--- a/res/values-w230dp-h220dp/styles.xml
+++ /dev/null
@@ -1,94 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  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.
-  -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <style name="DisplayTextStyle.Formula">
-        <item name="android:paddingTop">0dip</item>
-        <item name="android:paddingBottom">0dip</item>
-        <item name="android:paddingStart">16dip</item>
-        <item name="android:paddingEnd">16dip</item>
-        <item name="android:gravity">bottom</item>
-        <item name="minTextSize">28dip</item>
-        <item name="maxTextSize">28dip</item>
-        <item name="stepTextSize">8dip</item>
-    </style>
-
-    <style name="DisplayTextStyle.Result">
-        <item name="android:paddingTop">0dip</item>
-        <item name="android:paddingBottom">0dip</item>
-        <item name="android:paddingStart">16dip</item>
-        <item name="android:paddingEnd">16dip</item>
-        <item name="android:gravity">bottom</item>
-        <item name="android:textSize">28dip</item>
-    </style>
-
-    <style name="PadButtonStyle.Advanced">
-        <item name="android:background">@drawable/pad_button_advanced_background</item>
-        <item name="android:textColor">@color/pad_button_advanced_text_color</item>
-        <item name="android:textSize">14dip</item>
-    </style>
-
-    <style name="PadButtonStyle.Advanced.Text">
-        <item name="android:textAllCaps">true</item>
-        <item name="android:textSize">12dip</item>
-    </style>
-
-    <style name="PadButtonStyle.Numeric">
-        <item name="android:textSize">16dip</item>
-    </style>
-
-    <style name="PadButtonStyle.Numeric.Equals">
-        <item name="android:visibility">visible</item>
-    </style>
-
-    <style name="PadButtonStyle.Operator">
-        <item name="android:textSize">14dip</item>
-    </style>
-
-    <style name="PadButtonStyle.Operator.Text">
-        <item name="android:textAllCaps">true</item>
-        <item name="android:textSize">12dip</item>
-    </style>
-
-    <style name="PadLayoutStyle.Advanced">
-        <item name="android:elevation">4dip</item>
-        <item name="android:paddingTop">2dip</item>
-        <item name="android:paddingBottom">8dip</item>
-        <item name="android:paddingStart">18dip</item>
-        <item name="android:paddingEnd">18dip</item>
-    </style>
-
-    <style name="PadLayoutStyle.Numeric">
-        <item name="android:layout_width">0dip</item>
-        <item name="android:layout_weight">7</item>
-        <item name="android:paddingTop">2dip</item>
-        <item name="android:paddingBottom">8dip</item>
-        <item name="android:paddingStart">8dip</item>
-        <item name="android:paddingEnd">8dip</item>
-    </style>
-
-    <style name="PadLayoutStyle.Operator">
-        <item name="android:layout_width">0dip</item>
-        <item name="android:layout_weight">3</item>
-        <item name="android:paddingTop">2dip</item>
-        <item name="android:paddingBottom">8dip</item>
-        <item name="android:paddingStart">4dip</item>
-        <item name="android:paddingEnd">28dip</item>
-    </style>
-
-</resources>
diff --git a/res/values-w230dp-h275dp/dimens.xml b/res/values-w230dp-h275dp/dimens.xml
new file mode 100644
index 0000000..7f8bf5e
--- /dev/null
+++ b/res/values-w230dp-h275dp/dimens.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<resources>
+    <!-- Dimens for display formula. -->
+    <dimen name="formula_padding_top">0dip</dimen>
+    <dimen name="formula_padding_bottom">8dip</dimen>
+    <dimen name="formula_padding_start">16dip</dimen>
+    <dimen name="formula_padding_end">16dip</dimen>
+    <dimen name="formula_min_textsize">28dip</dimen>
+    <dimen name="formula_max_textsize">28dip</dimen>
+    <dimen name="formula_step_textsize">8dip</dimen>
+
+    <!-- Dimens for display result. -->
+    <dimen name="result_padding_top">0dip</dimen>
+    <dimen name="result_padding_bottom">8dip</dimen>
+    <dimen name="result_padding_start">16dip</dimen>
+    <dimen name="result_padding_end">16dip</dimen>
+    <dimen name="result_textsize">28dip</dimen>
+</resources>
diff --git a/res/values-w230dp-h275dp/styles.xml b/res/values-w230dp-h275dp/styles.xml
index 2f715a3..bbb38be 100644
--- a/res/values-w230dp-h275dp/styles.xml
+++ b/res/values-w230dp-h275dp/styles.xml
@@ -17,26 +17,6 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <style name="DisplayTextStyle.Formula">
-        <item name="android:paddingTop">0dip</item>
-        <item name="android:paddingBottom">0dip</item>
-        <item name="android:paddingStart">16dip</item>
-        <item name="android:paddingEnd">16dip</item>
-        <item name="android:gravity">bottom</item>
-        <item name="minTextSize">28dip</item>
-        <item name="maxTextSize">28dip</item>
-        <item name="stepTextSize">8dip</item>
-    </style>
-
-    <style name="DisplayTextStyle.Result">
-        <item name="android:paddingTop">0dip</item>
-        <item name="android:paddingBottom">0dip</item>
-        <item name="android:paddingStart">16dip</item>
-        <item name="android:paddingEnd">16dip</item>
-        <item name="android:gravity">bottom</item>
-        <item name="android:textSize">28dip</item>
-    </style>
-
     <style name="PadButtonStyle.Advanced">
         <item name="android:background">@drawable/pad_button_advanced_background</item>
         <item name="android:textColor">@color/pad_button_advanced_text_color</item>
diff --git a/res/values-w230dp-h375dp/dimens.xml b/res/values-w230dp-h375dp/dimens.xml
new file mode 100644
index 0000000..a747231
--- /dev/null
+++ b/res/values-w230dp-h375dp/dimens.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<resources>
+    <!-- Dimens for display formula. -->
+    <dimen name="formula_padding_top">2dip</dimen>
+    <dimen name="formula_padding_bottom">10dip</dimen>
+    <dimen name="formula_padding_start">16dip</dimen>
+    <dimen name="formula_padding_end">16dip</dimen>
+    <dimen name="formula_min_textsize">32dip</dimen>
+    <dimen name="formula_max_textsize">32dip</dimen>
+    <dimen name="formula_step_textsize">8dip</dimen>
+
+    <!-- Dimens for display result. -->
+    <dimen name="result_padding_top">12dip</dimen>
+    <dimen name="result_padding_bottom">18dip</dimen>
+    <dimen name="result_padding_start">16dip</dimen>
+    <dimen name="result_padding_end">16dip</dimen>
+    <dimen name="result_textsize">32dip</dimen>
+</resources>
\ No newline at end of file
diff --git a/res/values-w230dp-h375dp/styles.xml b/res/values-w230dp-h375dp/styles.xml
index 4aa32d8..c72fd3f 100644
--- a/res/values-w230dp-h375dp/styles.xml
+++ b/res/values-w230dp-h375dp/styles.xml
@@ -17,24 +17,6 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <style name="DisplayTextStyle.Formula">
-        <item name="android:paddingTop">2dip</item>
-        <item name="android:paddingBottom">10dip</item>
-        <item name="android:paddingStart">16dip</item>
-        <item name="android:paddingEnd">16dip</item>
-        <item name="minTextSize">32dip</item>
-        <item name="maxTextSize">32dip</item>
-        <item name="stepTextSize">8dip</item>
-    </style>
-
-    <style name="DisplayTextStyle.Result">
-        <item name="android:paddingTop">12dip</item>
-        <item name="android:paddingBottom">18dip</item>
-        <item name="android:paddingStart">16dip</item>
-        <item name="android:paddingEnd">16dip</item>
-        <item name="android:textSize">32dip</item>
-    </style>
-
     <style name="PadButtonStyle.Advanced">
         <item name="android:background">@drawable/pad_button_advanced_background</item>
         <item name="android:textColor">@color/pad_button_advanced_text_color</item>
diff --git a/res/values-w230dp-h475dp-port/dimens.xml b/res/values-w230dp-h475dp-port/dimens.xml
new file mode 100644
index 0000000..072a06e
--- /dev/null
+++ b/res/values-w230dp-h475dp-port/dimens.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<resources>
+    <!-- Dimens for display formula. -->
+    <dimen name="formula_padding_top">8dip</dimen>
+    <dimen name="formula_padding_bottom">18dip</dimen>
+    <dimen name="formula_padding_start">16dip</dimen>
+    <dimen name="formula_padding_end">16dip</dimen>
+    <dimen name="formula_min_textsize">32dip</dimen>
+    <dimen name="formula_max_textsize">56dip</dimen>
+    <dimen name="formula_step_textsize">8dip</dimen>
+
+    <!-- Dimens for display result. -->
+    <dimen name="result_padding_top">18dip</dimen>
+    <dimen name="result_padding_bottom">36dip</dimen>
+    <dimen name="result_padding_start">16dip</dimen>
+    <dimen name="result_padding_end">16dip</dimen>
+    <dimen name="result_textsize">32dip</dimen>
+</resources>
\ No newline at end of file
diff --git a/res/values-w230dp-h475dp-port/styles.xml b/res/values-w230dp-h475dp-port/styles.xml
index 050de1e..05987ad 100644
--- a/res/values-w230dp-h475dp-port/styles.xml
+++ b/res/values-w230dp-h475dp-port/styles.xml
@@ -17,24 +17,6 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <style name="DisplayTextStyle.Formula">
-        <item name="android:paddingTop">8dip</item>
-        <item name="android:paddingBottom">18dip</item>
-        <item name="android:paddingStart">16dip</item>
-        <item name="android:paddingEnd">16dip</item>
-        <item name="minTextSize">32dip</item>
-        <item name="maxTextSize">56dip</item>
-        <item name="stepTextSize">8dip</item>
-    </style>
-
-    <style name="DisplayTextStyle.Result">
-        <item name="android:paddingTop">18dip</item>
-        <item name="android:paddingBottom">36dip</item>
-        <item name="android:paddingStart">16dip</item>
-        <item name="android:paddingEnd">16dip</item>
-        <item name="android:textSize">32dip</item>
-    </style>
-
     <style name="PadButtonStyle.Advanced">
         <item name="android:background">@drawable/pad_button_advanced_background</item>
         <item name="android:textColor">@color/pad_button_advanced_text_color</item>
diff --git a/res/values-w375dp-h220dp/dimens.xml b/res/values-w375dp-h220dp/dimens.xml
new file mode 100644
index 0000000..10a6c21
--- /dev/null
+++ b/res/values-w375dp-h220dp/dimens.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<resources>
+    <!-- Dimens for display formula. -->
+    <dimen name="formula_padding_top">0dip</dimen>
+    <dimen name="formula_padding_bottom">8dip</dimen>
+    <dimen name="formula_padding_start">16dip</dimen>
+    <dimen name="formula_padding_end">16dip</dimen>
+    <dimen name="formula_min_textsize">28dip</dimen>
+    <dimen name="formula_max_textsize">28dip</dimen>
+    <dimen name="formula_step_textsize">8dip</dimen>
+
+    <!-- Dimens for display result. -->
+    <dimen name="result_padding_top">0dip</dimen>
+    <dimen name="result_padding_bottom">8dip</dimen>
+    <dimen name="result_padding_start">16dip</dimen>
+    <dimen name="result_padding_end">16dip</dimen>
+    <dimen name="result_textsize">28dip</dimen>
+</resources>
\ No newline at end of file
diff --git a/res/values-w375dp-h220dp/styles.xml b/res/values-w375dp-h220dp/styles.xml
index 3c6fe85..16931ff 100644
--- a/res/values-w375dp-h220dp/styles.xml
+++ b/res/values-w375dp-h220dp/styles.xml
@@ -17,26 +17,6 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <style name="DisplayTextStyle.Formula">
-        <item name="android:paddingTop">0dip</item>
-        <item name="android:paddingBottom">0dip</item>
-        <item name="android:paddingStart">16dip</item>
-        <item name="android:paddingEnd">16dip</item>
-        <item name="android:gravity">bottom</item>
-        <item name="minTextSize">28dip</item>
-        <item name="maxTextSize">28dip</item>
-        <item name="stepTextSize">8dip</item>
-    </style>
-
-    <style name="DisplayTextStyle.Result">
-        <item name="android:paddingTop">0dip</item>
-        <item name="android:paddingBottom">0dip</item>
-        <item name="android:paddingStart">16dip</item>
-        <item name="android:paddingEnd">16dip</item>
-        <item name="android:gravity">bottom</item>
-        <item name="android:textSize">28dip</item>
-    </style>
-
     <style name="PadButtonStyle.Advanced">
         <item name="android:background">@drawable/pad_button_advanced_background</item>
         <item name="android:textColor">@color/pad_button_advanced_text_color</item>
diff --git a/res/values-w375dp-h275dp/dimens.xml b/res/values-w375dp-h275dp/dimens.xml
new file mode 100644
index 0000000..10a6c21
--- /dev/null
+++ b/res/values-w375dp-h275dp/dimens.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<resources>
+    <!-- Dimens for display formula. -->
+    <dimen name="formula_padding_top">0dip</dimen>
+    <dimen name="formula_padding_bottom">8dip</dimen>
+    <dimen name="formula_padding_start">16dip</dimen>
+    <dimen name="formula_padding_end">16dip</dimen>
+    <dimen name="formula_min_textsize">28dip</dimen>
+    <dimen name="formula_max_textsize">28dip</dimen>
+    <dimen name="formula_step_textsize">8dip</dimen>
+
+    <!-- Dimens for display result. -->
+    <dimen name="result_padding_top">0dip</dimen>
+    <dimen name="result_padding_bottom">8dip</dimen>
+    <dimen name="result_padding_start">16dip</dimen>
+    <dimen name="result_padding_end">16dip</dimen>
+    <dimen name="result_textsize">28dip</dimen>
+</resources>
\ No newline at end of file
diff --git a/res/values-w375dp-h275dp/styles.xml b/res/values-w375dp-h275dp/styles.xml
index c628645..efed4dc 100644
--- a/res/values-w375dp-h275dp/styles.xml
+++ b/res/values-w375dp-h275dp/styles.xml
@@ -17,26 +17,6 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <style name="DisplayTextStyle.Formula">
-        <item name="android:paddingTop">0dip</item>
-        <item name="android:paddingBottom">0dip</item>
-        <item name="android:paddingStart">16dip</item>
-        <item name="android:paddingEnd">16dip</item>
-        <item name="android:gravity">bottom</item>
-        <item name="minTextSize">28dip</item>
-        <item name="maxTextSize">28dip</item>
-        <item name="stepTextSize">8dip</item>
-    </style>
-
-    <style name="DisplayTextStyle.Result">
-        <item name="android:paddingTop">0dip</item>
-        <item name="android:paddingBottom">0dip</item>
-        <item name="android:paddingStart">16dip</item>
-        <item name="android:paddingEnd">16dip</item>
-        <item name="android:gravity">bottom</item>
-        <item name="android:textSize">28dip</item>
-    </style>
-
     <style name="PadButtonStyle.Advanced">
         <item name="android:background">@drawable/pad_button_advanced_background</item>
         <item name="android:textColor">@color/pad_button_advanced_text_color</item>
diff --git a/res/values-w375dp-h375dp/dimens.xml b/res/values-w375dp-h375dp/dimens.xml
new file mode 100644
index 0000000..a747231
--- /dev/null
+++ b/res/values-w375dp-h375dp/dimens.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<resources>
+    <!-- Dimens for display formula. -->
+    <dimen name="formula_padding_top">2dip</dimen>
+    <dimen name="formula_padding_bottom">10dip</dimen>
+    <dimen name="formula_padding_start">16dip</dimen>
+    <dimen name="formula_padding_end">16dip</dimen>
+    <dimen name="formula_min_textsize">32dip</dimen>
+    <dimen name="formula_max_textsize">32dip</dimen>
+    <dimen name="formula_step_textsize">8dip</dimen>
+
+    <!-- Dimens for display result. -->
+    <dimen name="result_padding_top">12dip</dimen>
+    <dimen name="result_padding_bottom">18dip</dimen>
+    <dimen name="result_padding_start">16dip</dimen>
+    <dimen name="result_padding_end">16dip</dimen>
+    <dimen name="result_textsize">32dip</dimen>
+</resources>
\ No newline at end of file
diff --git a/res/values-w375dp-h375dp/styles.xml b/res/values-w375dp-h375dp/styles.xml
index 14e96ca..b1d4ac5 100644
--- a/res/values-w375dp-h375dp/styles.xml
+++ b/res/values-w375dp-h375dp/styles.xml
@@ -17,24 +17,6 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <style name="DisplayTextStyle.Formula">
-        <item name="android:paddingTop">2dip</item>
-        <item name="android:paddingBottom">10dip</item>
-        <item name="android:paddingStart">16dip</item>
-        <item name="android:paddingEnd">16dip</item>
-        <item name="minTextSize">32dip</item>
-        <item name="maxTextSize">32dip</item>
-        <item name="stepTextSize">8dip</item>
-    </style>
-
-    <style name="DisplayTextStyle.Result">
-        <item name="android:paddingTop">12dip</item>
-        <item name="android:paddingBottom">18dip</item>
-        <item name="android:paddingStart">16dip</item>
-        <item name="android:paddingEnd">16dip</item>
-        <item name="android:textSize">32dip</item>
-    </style>
-
     <style name="PadButtonStyle.Advanced">
         <item name="android:background">@drawable/pad_button_advanced_background</item>
         <item name="android:textColor">@color/pad_button_advanced_text_color</item>
diff --git a/res/values-w375dp-h500dp-port/dimens.xml b/res/values-w375dp-h500dp-port/dimens.xml
new file mode 100644
index 0000000..37508de
--- /dev/null
+++ b/res/values-w375dp-h500dp-port/dimens.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<resources>
+    <!-- Dimens for display formula. -->
+    <dimen name="formula_padding_top">16dip</dimen>
+    <dimen name="formula_padding_bottom">28dip</dimen>
+    <dimen name="formula_padding_start">16dip</dimen>
+    <dimen name="formula_padding_end">16dip</dimen>
+    <dimen name="formula_min_textsize">42dip</dimen>
+    <dimen name="formula_max_textsize">74dip</dimen>
+    <dimen name="formula_step_textsize">8dip</dimen>
+
+    <!-- Dimens for display result. -->
+    <dimen name="result_padding_top">16dip</dimen>
+    <dimen name="result_padding_bottom">42dip</dimen>
+    <dimen name="result_padding_start">16dip</dimen>
+    <dimen name="result_padding_end">16dip</dimen>
+    <dimen name="result_textsize">42dip</dimen>
+</resources>
\ No newline at end of file
diff --git a/res/values-w375dp-h500dp-port/styles.xml b/res/values-w375dp-h500dp-port/styles.xml
index 066aa8e..5f5057e 100644
--- a/res/values-w375dp-h500dp-port/styles.xml
+++ b/res/values-w375dp-h500dp-port/styles.xml
@@ -17,24 +17,6 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <style name="DisplayTextStyle.Formula">
-        <item name="android:paddingTop">16dip</item>
-        <item name="android:paddingBottom">28dip</item>
-        <item name="android:paddingStart">16dip</item>
-        <item name="android:paddingEnd">16dip</item>
-        <item name="minTextSize">42dip</item>
-        <item name="maxTextSize">74dip</item>
-        <item name="stepTextSize">8dip</item>
-    </style>
-
-    <style name="DisplayTextStyle.Result">
-        <item name="android:paddingTop">16dip</item>
-        <item name="android:paddingBottom">42dip</item>
-        <item name="android:paddingStart">16dip</item>
-        <item name="android:paddingEnd">16dip</item>
-        <item name="android:textSize">42dip</item>
-    </style>
-
     <style name="PadButtonStyle.Advanced">
         <item name="android:background">@drawable/pad_button_advanced_background</item>
         <item name="android:textColor">@color/pad_button_advanced_text_color</item>
diff --git a/res/values-w375dp-h768dp-port/dimens.xml b/res/values-w375dp-h768dp-port/dimens.xml
new file mode 100644
index 0000000..e74d35d
--- /dev/null
+++ b/res/values-w375dp-h768dp-port/dimens.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<resources>
+    <!-- Dimens for display formula. -->
+    <dimen name="formula_padding_top">16dip</dimen>
+    <dimen name="formula_padding_bottom">32dip</dimen>
+    <dimen name="formula_padding_start">44dip</dimen>
+    <dimen name="formula_padding_end">44dip</dimen>
+    <dimen name="formula_min_textsize">48dip</dimen>
+    <dimen name="formula_max_textsize">72dip</dimen>
+    <dimen name="formula_step_textsize">8dip</dimen>
+
+    <!-- Dimens for display result. -->
+    <dimen name="result_padding_top">20dip</dimen>
+    <dimen name="result_padding_bottom">48dip</dimen>
+    <dimen name="result_padding_start">44dip</dimen>
+    <dimen name="result_padding_end">44dip</dimen>
+    <dimen name="result_textsize">48dip</dimen>
+</resources>
\ No newline at end of file
diff --git a/res/values-w375dp-h768dp-port/styles.xml b/res/values-w375dp-h768dp-port/styles.xml
index ec91033..2c5ae35 100644
--- a/res/values-w375dp-h768dp-port/styles.xml
+++ b/res/values-w375dp-h768dp-port/styles.xml
@@ -17,24 +17,6 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <style name="DisplayTextStyle.Formula">
-        <item name="android:paddingTop">16dip</item>
-        <item name="android:paddingBottom">32dip</item>
-        <item name="android:paddingStart">44dip</item>
-        <item name="android:paddingEnd">44dip</item>
-        <item name="minTextSize">48dip</item>
-        <item name="maxTextSize">72dip</item>
-        <item name="stepTextSize">8dip</item>
-    </style>
-
-    <style name="DisplayTextStyle.Result">
-        <item name="android:paddingTop">20dip</item>
-        <item name="android:paddingBottom">48dip</item>
-        <item name="android:paddingStart">44dip</item>
-        <item name="android:paddingEnd">44dip</item>
-        <item name="android:textSize">48dip</item>
-    </style>
-
     <style name="PadButtonStyle.Advanced">
         <item name="android:background">@drawable/pad_button_advanced_background</item>
         <item name="android:textColor">@color/pad_button_advanced_text_color</item>
diff --git a/res/values-w520dp-h220dp-land/dimens.xml b/res/values-w520dp-h220dp-land/dimens.xml
new file mode 100644
index 0000000..0ae75b5
--- /dev/null
+++ b/res/values-w520dp-h220dp-land/dimens.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<resources>
+    <!-- Dimens for display formula. -->
+    <dimen name="formula_padding_top">0dip</dimen>
+    <dimen name="formula_padding_bottom">8dip</dimen>
+    <dimen name="formula_padding_start">36dip</dimen>
+    <dimen name="formula_padding_end">36dip</dimen>
+    <dimen name="formula_min_textsize">28dip</dimen>
+    <dimen name="formula_max_textsize">28dip</dimen>
+    <dimen name="formula_step_textsize">8dip</dimen>
+
+    <!-- Dimens for display result. -->
+    <dimen name="result_padding_top">0dip</dimen>
+    <dimen name="result_padding_bottom">8dip</dimen>
+    <dimen name="result_padding_start">36dip</dimen>
+    <dimen name="result_padding_end">36dip</dimen>
+    <dimen name="result_textsize">28dip</dimen>
+</resources>
\ No newline at end of file
diff --git a/res/values-w520dp-h220dp-land/styles.xml b/res/values-w520dp-h220dp-land/styles.xml
index e90e530..536d5e7 100644
--- a/res/values-w520dp-h220dp-land/styles.xml
+++ b/res/values-w520dp-h220dp-land/styles.xml
@@ -17,26 +17,6 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <style name="DisplayTextStyle.Formula">
-        <item name="android:paddingTop">0dip</item>
-        <item name="android:paddingBottom">0dip</item>
-        <item name="android:paddingStart">36dip</item>
-        <item name="android:paddingEnd">36dip</item>
-        <item name="android:gravity">bottom</item>
-        <item name="minTextSize">28dip</item>
-        <item name="maxTextSize">28dip</item>
-        <item name="stepTextSize">8dip</item>
-    </style>
-
-    <style name="DisplayTextStyle.Result">
-        <item name="android:paddingTop">0dip</item>
-        <item name="android:paddingBottom">0dip</item>
-        <item name="android:paddingStart">36dip</item>
-        <item name="android:paddingEnd">36dip</item>
-        <item name="android:gravity">bottom</item>
-        <item name="android:textSize">28dip</item>
-    </style>
-
     <style name="PadButtonStyle.Advanced">
         <item name="android:background">@drawable/pad_button_advanced_background</item>
         <item name="android:textColor">@color/pad_button_advanced_text_color</item>
diff --git a/res/values-w520dp-h275dp-land/dimens.xml b/res/values-w520dp-h275dp-land/dimens.xml
new file mode 100644
index 0000000..b00f26e
--- /dev/null
+++ b/res/values-w520dp-h275dp-land/dimens.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<resources>
+    <!-- Dimens for display formula. -->
+    <dimen name="formula_padding_top">2dip</dimen>
+    <dimen name="formula_padding_bottom">8dip</dimen>
+    <dimen name="formula_padding_start">36dip</dimen>
+    <dimen name="formula_padding_end">36dip</dimen>
+    <dimen name="formula_min_textsize">28dip</dimen>
+    <dimen name="formula_max_textsize">28dip</dimen>
+    <dimen name="formula_step_textsize">8dip</dimen>
+
+    <!-- Dimens for display result. -->
+    <dimen name="result_padding_top">8dip</dimen>
+    <dimen name="result_padding_bottom">16dip</dimen>
+    <dimen name="result_padding_start">36dip</dimen>
+    <dimen name="result_padding_end">36dip</dimen>
+    <dimen name="result_textsize">28dip</dimen>
+</resources>
\ No newline at end of file
diff --git a/res/values-w520dp-h275dp-land/styles.xml b/res/values-w520dp-h275dp-land/styles.xml
index 9d66a9b..003949e 100644
--- a/res/values-w520dp-h275dp-land/styles.xml
+++ b/res/values-w520dp-h275dp-land/styles.xml
@@ -17,24 +17,6 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <style name="DisplayTextStyle.Formula">
-        <item name="android:paddingTop">2dip</item>
-        <item name="android:paddingBottom">8dip</item>
-        <item name="android:paddingStart">36dip</item>
-        <item name="android:paddingEnd">36dip</item>
-        <item name="minTextSize">28dip</item>
-        <item name="maxTextSize">28dip</item>
-        <item name="stepTextSize">8dip</item>
-    </style>
-
-    <style name="DisplayTextStyle.Result">
-        <item name="android:paddingTop">8dip</item>
-        <item name="android:paddingBottom">16dip</item>
-        <item name="android:paddingStart">36dip</item>
-        <item name="android:paddingEnd">36dip</item>
-        <item name="android:textSize">28dip</item>
-    </style>
-
     <style name="PadButtonStyle.Advanced">
         <item name="android:background">@drawable/pad_button_advanced_background</item>
         <item name="android:textColor">@color/pad_button_advanced_text_color</item>
diff --git a/res/values-w520dp-h375dp-land/dimens.xml b/res/values-w520dp-h375dp-land/dimens.xml
new file mode 100644
index 0000000..7cfeb8e
--- /dev/null
+++ b/res/values-w520dp-h375dp-land/dimens.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<resources>
+    <!-- Dimens for display formula. -->
+    <dimen name="formula_padding_top">2dip</dimen>
+    <dimen name="formula_padding_bottom">10dip</dimen>
+    <dimen name="formula_padding_start">36dip</dimen>
+    <dimen name="formula_padding_end">36dip</dimen>
+    <dimen name="formula_min_textsize">32dip</dimen>
+    <dimen name="formula_max_textsize">32dip</dimen>
+    <dimen name="formula_step_textsize">8dip</dimen>
+
+    <!-- Dimens for display result. -->
+    <dimen name="result_padding_top">12dip</dimen>
+    <dimen name="result_padding_bottom">18dip</dimen>
+    <dimen name="result_padding_start">36dip</dimen>
+    <dimen name="result_padding_end">36dip</dimen>
+    <dimen name="result_textsize">32dip</dimen>
+</resources>
\ No newline at end of file
diff --git a/res/values-w520dp-h375dp-land/styles.xml b/res/values-w520dp-h375dp-land/styles.xml
index d89ea24..8c7d0ed 100644
--- a/res/values-w520dp-h375dp-land/styles.xml
+++ b/res/values-w520dp-h375dp-land/styles.xml
@@ -17,24 +17,6 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <style name="DisplayTextStyle.Formula">
-        <item name="android:paddingTop">2dip</item>
-        <item name="android:paddingBottom">10dip</item>
-        <item name="android:paddingStart">36dip</item>
-        <item name="android:paddingEnd">36dip</item>
-        <item name="minTextSize">32dip</item>
-        <item name="maxTextSize">32dip</item>
-        <item name="stepTextSize">8dip</item>
-    </style>
-
-    <style name="DisplayTextStyle.Result">
-        <item name="android:paddingTop">12dip</item>
-        <item name="android:paddingBottom">18dip</item>
-        <item name="android:paddingStart">36dip</item>
-        <item name="android:paddingEnd">36dip</item>
-        <item name="android:textSize">32dip</item>
-    </style>
-
     <style name="PadButtonStyle.Advanced">
         <item name="android:background">@drawable/pad_button_advanced_background</item>
         <item name="android:textColor">@color/pad_button_advanced_text_color</item>
diff --git a/res/values-w520dp-h500dp-land/dimens.xml b/res/values-w520dp-h500dp-land/dimens.xml
new file mode 100644
index 0000000..37508de
--- /dev/null
+++ b/res/values-w520dp-h500dp-land/dimens.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<resources>
+    <!-- Dimens for display formula. -->
+    <dimen name="formula_padding_top">16dip</dimen>
+    <dimen name="formula_padding_bottom">28dip</dimen>
+    <dimen name="formula_padding_start">16dip</dimen>
+    <dimen name="formula_padding_end">16dip</dimen>
+    <dimen name="formula_min_textsize">42dip</dimen>
+    <dimen name="formula_max_textsize">74dip</dimen>
+    <dimen name="formula_step_textsize">8dip</dimen>
+
+    <!-- Dimens for display result. -->
+    <dimen name="result_padding_top">16dip</dimen>
+    <dimen name="result_padding_bottom">42dip</dimen>
+    <dimen name="result_padding_start">16dip</dimen>
+    <dimen name="result_padding_end">16dip</dimen>
+    <dimen name="result_textsize">42dip</dimen>
+</resources>
\ No newline at end of file
diff --git a/res/values-w520dp-h500dp-land/styles.xml b/res/values-w520dp-h500dp-land/styles.xml
index 883f6cd..e0cc0e2 100644
--- a/res/values-w520dp-h500dp-land/styles.xml
+++ b/res/values-w520dp-h500dp-land/styles.xml
@@ -17,24 +17,6 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <style name="DisplayTextStyle.Formula">
-        <item name="android:paddingTop">16dip</item>
-        <item name="android:paddingBottom">28dip</item>
-        <item name="android:paddingStart">16dip</item>
-        <item name="android:paddingEnd">16dip</item>
-        <item name="minTextSize">42dip</item>
-        <item name="maxTextSize">74dip</item>
-        <item name="stepTextSize">8dip</item>
-    </style>
-
-    <style name="DisplayTextStyle.Result">
-        <item name="android:paddingTop">16dip</item>
-        <item name="android:paddingBottom">42dip</item>
-        <item name="android:paddingStart">16dip</item>
-        <item name="android:paddingEnd">16dip</item>
-        <item name="android:textSize">42dip</item>
-    </style>
-
     <style name="PadButtonStyle.Advanced">
         <item name="android:background">@drawable/pad_button_advanced_background</item>
         <item name="android:textColor">@color/pad_button_advanced_text_color</item>
diff --git a/res/values-w520dp-h768dp-land/dimens.xml b/res/values-w520dp-h768dp-land/dimens.xml
new file mode 100644
index 0000000..69b2f01
--- /dev/null
+++ b/res/values-w520dp-h768dp-land/dimens.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<resources>
+    <!-- Dimens for display formula. -->
+    <dimen name="formula_padding_top">24dip</dimen>
+    <dimen name="formula_padding_bottom">32dip</dimen>
+    <dimen name="formula_padding_start">44dip</dimen>
+    <dimen name="formula_padding_end">44dip</dimen>
+    <dimen name="formula_min_textsize">44dip</dimen>
+    <dimen name="formula_max_textsize">76dip</dimen>
+    <dimen name="formula_step_textsize">8dip</dimen>
+
+    <!-- Dimens for display result. -->
+    <dimen name="result_padding_top">24dip</dimen>
+    <dimen name="result_padding_bottom">56dip</dimen>
+    <dimen name="result_padding_start">44dip</dimen>
+    <dimen name="result_padding_end">44dip</dimen>
+    <dimen name="result_textsize">44dip</dimen>
+</resources>
\ No newline at end of file
diff --git a/res/values-w520dp-h768dp-land/styles.xml b/res/values-w520dp-h768dp-land/styles.xml
index 9fdf68a..61acbd2 100644
--- a/res/values-w520dp-h768dp-land/styles.xml
+++ b/res/values-w520dp-h768dp-land/styles.xml
@@ -17,24 +17,6 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <style name="DisplayTextStyle.Formula">
-        <item name="android:paddingTop">24dip</item>
-        <item name="android:paddingBottom">32dip</item>
-        <item name="android:paddingStart">44dip</item>
-        <item name="android:paddingEnd">44dip</item>
-        <item name="minTextSize">44dip</item>
-        <item name="maxTextSize">76dip</item>
-        <item name="stepTextSize">8dip</item>
-    </style>
-
-    <style name="DisplayTextStyle.Result">
-        <item name="android:paddingTop">24dip</item>
-        <item name="android:paddingBottom">56dip</item>
-        <item name="android:paddingStart">44dip</item>
-        <item name="android:paddingEnd">44dip</item>
-        <item name="android:textSize">44dip</item>
-    </style>
-
     <style name="PadButtonStyle.Advanced">
         <item name="android:background">@drawable/pad_button_advanced_background</item>
         <item name="android:textColor">@color/pad_button_advanced_text_color</item>
diff --git a/res/values-w520dp-h768dp-port/dimens.xml b/res/values-w520dp-h768dp-port/dimens.xml
new file mode 100644
index 0000000..e74d35d
--- /dev/null
+++ b/res/values-w520dp-h768dp-port/dimens.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<resources>
+    <!-- Dimens for display formula. -->
+    <dimen name="formula_padding_top">16dip</dimen>
+    <dimen name="formula_padding_bottom">32dip</dimen>
+    <dimen name="formula_padding_start">44dip</dimen>
+    <dimen name="formula_padding_end">44dip</dimen>
+    <dimen name="formula_min_textsize">48dip</dimen>
+    <dimen name="formula_max_textsize">72dip</dimen>
+    <dimen name="formula_step_textsize">8dip</dimen>
+
+    <!-- Dimens for display result. -->
+    <dimen name="result_padding_top">20dip</dimen>
+    <dimen name="result_padding_bottom">48dip</dimen>
+    <dimen name="result_padding_start">44dip</dimen>
+    <dimen name="result_padding_end">44dip</dimen>
+    <dimen name="result_textsize">48dip</dimen>
+</resources>
\ No newline at end of file
diff --git a/res/values-w520dp-h768dp-port/styles.xml b/res/values-w520dp-h768dp-port/styles.xml
index 4d5e2db..9136b50 100644
--- a/res/values-w520dp-h768dp-port/styles.xml
+++ b/res/values-w520dp-h768dp-port/styles.xml
@@ -17,24 +17,6 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <style name="DisplayTextStyle.Formula">
-        <item name="android:paddingTop">16dip</item>
-        <item name="android:paddingBottom">32dip</item>
-        <item name="android:paddingStart">44dip</item>
-        <item name="android:paddingEnd">44dip</item>
-        <item name="minTextSize">48dip</item>
-        <item name="maxTextSize">72dip</item>
-        <item name="stepTextSize">8dip</item>
-    </style>
-
-    <style name="DisplayTextStyle.Result">
-        <item name="android:paddingTop">20dip</item>
-        <item name="android:paddingBottom">48dip</item>
-        <item name="android:paddingStart">44dip</item>
-        <item name="android:paddingEnd">44dip</item>
-        <item name="android:textSize">48dip</item>
-    </style>
-
     <style name="PadButtonStyle.Advanced">
         <item name="android:background">@drawable/pad_button_advanced_background</item>
         <item name="android:textColor">@color/pad_button_advanced_text_color</item>
diff --git a/res/values/attr.xml b/res/values/attr.xml
index cfefc9d..825fc9f 100644
--- a/res/values/attr.xml
+++ b/res/values/attr.xml
@@ -17,7 +17,7 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <declare-styleable name="CalculatorText">
+    <declare-styleable name="CalculatorFormula">
         <attr name="minTextSize" format="dimension" />
         <attr name="maxTextSize" format="dimension" />
         <attr name="stepTextSize" format="dimension" />
diff --git a/res/values/color.xml b/res/values/color.xml
index d63dcea..2f6475f 100644
--- a/res/values/color.xml
+++ b/res/values/color.xml
@@ -17,9 +17,15 @@
 
 <resources>
 
-    <!-- Default background color for the status bar. -->
+    <!-- Default accent color. -->
     <color name="calculator_accent_color">#0097A7</color>
 
+    <!-- Primary color (Color for the toolbars). -->
+    <color name="calculator_primary_color">#00BCD4</color>
+
+    <!-- Status bar color. -->
+    <color name="calculator_statusbar_color">#0096A9</color>
+
     <!-- Color to indicate an error has occured. -->
     <color name="calculator_error_color">#C2185B</color>
 
@@ -27,7 +33,7 @@
     <color name="display_background_color">#FFF</color>
 
     <!-- Text color for the formula in the calculator display. -->
-    <color name="display_formula_text_color">#8A000000</color>
+    <color name="display_formula_text_color">#000000</color>
 
     <!-- Text color for the result in the calculator display. -->
     <color name="display_result_text_color">#6C000000</color>
@@ -56,4 +62,7 @@
     <!-- Ripple color when a button is pressed in a pad. -->
     <color name="pad_button_advanced_ripple_color">#1A000000</color>
 
+    <!-- Background color for empty history view. -->
+    <color name="empty_history_color">#EEEEEE</color>
+
 </resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 5218acd..827097a 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -20,4 +20,25 @@
     <!-- The margin between the pad pages when displayed using a view pager. -->
     <dimen name="pad_page_margin">24dip</dimen>
 
-</resources>
+    <dimen name="history_divider_padding">14dip</dimen>
+
+    <dimen name="history_item_text_padding_top">8dip</dimen>
+    <dimen name="history_item_text_padding_bottom">16dip</dimen>
+
+    <!-- Dimens for display formula. -->
+    <dimen name="formula_padding_top">0dip</dimen>
+    <dimen name="formula_padding_bottom">8dip</dimen>
+    <dimen name="formula_padding_start">16dip</dimen>
+    <dimen name="formula_padding_end">16dip</dimen>
+    <dimen name="formula_min_textsize">28dip</dimen>
+    <dimen name="formula_max_textsize">28dip</dimen>
+    <dimen name="formula_step_textsize">8dip</dimen>
+
+    <!-- Dimens for display result. -->
+    <dimen name="result_padding_top">0dip</dimen>
+    <dimen name="result_padding_bottom">8dip</dimen>
+    <dimen name="result_padding_start">16dip</dimen>
+    <dimen name="result_padding_end">16dip</dimen>
+    <dimen name="result_textsize">28dip</dimen>
+
+</resources>
\ No newline at end of file
diff --git a/res/values-w230dp-h220dp/layout.xml b/res/values/layout.xml
similarity index 100%
rename from res/values-w230dp-h220dp/layout.xml
rename to res/values/layout.xml
diff --git a/res/values/strings.xml b/res/values/strings.xml
index d3d6b18..762378a 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -127,6 +127,22 @@
     <!-- Toggle button to show/hide inverse functions. [CHAR_LIMIT=4] -->
     <string name="inv" translatable="false">inv</string>
 
+    <!--
+      Item on Formula context menu used to paste from the Memory into the edit field. [CHAR_LIMIT=2]
+      -->
+    <string name="memory_recall" translatable="false">MR</string>
+    <!-- Item on Result context menu used to store the result in memory. [CHAR_LIMIT=2]
+      -->
+    <string name="memory_store" translatable="false">MS</string>
+    <!-- Item on Result context menu, which subtracts the current result from the number in memory.
+      [CHAR_LIMIT=2]
+      -->
+    <string name="memory_subtract" translatable="false">M\u2212</string>
+    <!-- Item on Result context menu, which adds the current result to the number in memory.
+      [CHAR_LIMIT=2]
+      -->
+    <string name="memory_add" translatable="false">M+</string>
+
     <!-- Content description for 'e' button. [CHAR_LIMIT=NONE] -->
     <string name="desc_const_e">Euler\'s number</string>
     <!-- Content description for 'π' button. [CHAR_LIMIT=NONE] -->
@@ -215,6 +231,8 @@
 
     <!-- Content description for formula field when it is empty. [CHAR_LIMIT=NONE] -->
     <string name="desc_formula">No formula</string>
+    <!-- Content description for result field when it is empty. [CHAR_LIMIT=NONE] -->
+    <string name="desc_result">No result</string>
 
     <!-- Content description for the numeric/operation pad when slide-able. [CHAR_LIMIT=NONE] -->
     <string name="desc_num_pad">Numbers and basic operations</string>
@@ -248,9 +266,9 @@
     <string name="text_copied_toast">Text copied</string>
 
     <!-- Dialog message when a computation is cancelled by the user. [CHAR_LIMIT=NONE] -->
-    <string name="cancelled">Computation cancelled</string>
+    <string name="cancelled">Computation cancelled.</string>
     <!-- Dialog message when a computation times out. [CHAR_LIMIT=NONE] -->
-    <string name="timeout">Timed out. Value may be infinite or undefined.</string>
+    <string name="timeout">Value may be infinite or undefined.</string>
     <!--
       Button label to allow future computations with a longer timeout.
 
@@ -274,5 +292,27 @@
     <string name="menu_fraction">Answer as fraction</string>
     <!-- Menu option to view the app's open source licenses. [CHAR_LIMIT=40] -->
     <string name="menu_licenses">Open source licenses</string>
+    <!-- Menu option to access calculation history. [CHAR_LIMIT=40] -->
+    <string name="menu_history">History</string>
+    <!-- Menu option to clear calculation history and memory. [CHAR_LIMIT=40] -->
+    <string name="menu_clear_history">Clear</string>
+
+    <!-- Action bar title in history page. [CHAR_LIMIT=40] -->
+    <string name="title_history">History</string>
+    <!-- Action bar navigate up description in history page. [CHAR_LIMIT=40] -->
+    <string name="desc_navigate_up">Navigate up</string>
+
+    <!-- Title for alert dialog when calculation takes too long (timeout). [CHAR_LIMIT=30] -->
+    <string name="dialog_timeout">Timeout</string>
+
+    <!--
+      Message for alert dialog when user is about to clear history and memory. [CHAR_LIMIT=NONE]
+    -->
+    <string name="dialog_clear">Clear history and memory?</string>
+
+    <!-- Title for "current expression" in history page. [CHAR_LIMIT=40] -->
+    <string name="title_current_expression">Current Expression</string>
+    <!-- Placeholder string when there is no history to be shown. [CHAR_LIMIT=40] -->
+    <string name="no_history">No History</string>
 
 </resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 4935103..5f4a9f6 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -18,7 +18,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
     <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>
@@ -26,12 +25,39 @@
         <item name="android:textAlignment">viewEnd</item>
     </style>
 
+    <style name="DisplayTextStyle.Formula">
+        <item name="android:paddingTop">@dimen/formula_padding_top</item>
+        <item name="android:paddingBottom">@dimen/formula_padding_bottom</item>
+        <item name="android:paddingStart">@dimen/formula_padding_start</item>
+        <item name="android:paddingEnd">@dimen/formula_padding_end</item>
+        <item name="android:gravity">bottom</item>
+        <item name="minTextSize">@dimen/formula_min_textsize</item>
+        <item name="maxTextSize">@dimen/formula_max_textsize</item>
+        <item name="stepTextSize">@dimen/formula_step_textsize</item>
+    </style>
+
+    <style name="DisplayTextStyle.Result">
+        <item name="android:paddingTop">@dimen/result_padding_top</item>
+        <item name="android:paddingBottom">@dimen/result_padding_bottom</item>
+        <item name="android:paddingStart">@dimen/result_padding_start</item>
+        <item name="android:paddingEnd">@dimen/result_padding_end</item>
+        <item name="android:gravity">bottom</item>
+        <item name="android:textSize">@dimen/result_textsize</item>
+    </style>
+
+    <style name="HistoryItemTextStyle" parent="DisplayTextStyle">
+        <item name="android:layout_gravity">bottom|end</item>
+        <item name="android:paddingTop">@dimen/history_item_text_padding_top</item>
+        <item name="android:paddingBottom">@dimen/history_item_text_padding_bottom</item>
+        <!-- Note: result_padding_start == formula_padding_start. -->
+        <item name="android:paddingStart">@dimen/result_padding_start</item>
+        <item name="android:paddingEnd">@dimen/result_padding_end</item>
+        <item name="android:textSize">@dimen/result_textsize</item>
+    </style>
+
     <style name="PadButtonStyle" parent="@android:style/Widget.Material.Light.Button.Borderless">
         <item name="android:layout_width">0dip</item>
         <item name="android:layout_height">0dip</item>
-        <item name="android:layout_rowWeight">1</item>
-        <item name="android:layout_columnWeight">1</item>
-        <item name="android:layout_gravity">fill</item>
         <item name="android:background">@drawable/pad_button_background</item>
         <item name="android:fontFamily">sans-serif-light</item>
         <item name="android:gravity">center</item>
@@ -41,6 +67,39 @@
         <item name="android:onClick">onButtonClick</item>
         <item name="android:textAllCaps">false</item>
         <item name="android:textColor">@color/pad_button_text_color</item>
+
+        <!-- Attributes from android.support.v7.gridlayout -->
+        <item name="layout_gravity">fill</item>
+        <item name="layout_rowWeight">1</item>
+        <item name="layout_columnWeight">1</item>
+    </style>
+
+    <style name="PadButtonStyle.Advanced">
+        <item name="android:background">@drawable/pad_button_advanced_background</item>
+        <item name="android:textColor">@color/pad_button_advanced_text_color</item>
+        <item name="android:textSize">14dip</item>
+    </style>
+
+    <style name="PadButtonStyle.Advanced.Text">
+        <item name="android:textAllCaps">true</item>
+        <item name="android:textSize">12dip</item>
+    </style>
+
+    <style name="PadButtonStyle.Numeric">
+        <item name="android:textSize">16dip</item>
+    </style>
+
+    <style name="PadButtonStyle.Numeric.Equals">
+        <item name="android:visibility">visible</item>
+    </style>
+
+    <style name="PadButtonStyle.Operator">
+        <item name="android:textSize">14dip</item>
+    </style>
+
+    <style name="PadButtonStyle.Operator.Text">
+        <item name="android:textAllCaps">true</item>
+        <item name="android:textSize">12dip</item>
     </style>
 
     <style name="PadLayoutStyle">
@@ -48,4 +107,30 @@
         <item name="android:layout_height">match_parent</item>
     </style>
 
+    <style name="PadLayoutStyle.Advanced">
+        <item name="android:elevation">4dip</item>
+        <item name="android:paddingTop">2dip</item>
+        <item name="android:paddingBottom">8dip</item>
+        <item name="android:paddingStart">18dip</item>
+        <item name="android:paddingEnd">18dip</item>
+    </style>
+
+    <style name="PadLayoutStyle.Numeric">
+        <item name="android:layout_width">0dip</item>
+        <item name="android:layout_weight">7</item>
+        <item name="android:paddingTop">2dip</item>
+        <item name="android:paddingBottom">8dip</item>
+        <item name="android:paddingStart">8dip</item>
+        <item name="android:paddingEnd">8dip</item>
+    </style>
+
+    <style name="PadLayoutStyle.Operator">
+        <item name="android:layout_width">0dip</item>
+        <item name="android:layout_weight">3</item>
+        <item name="android:paddingTop">2dip</item>
+        <item name="android:paddingBottom">8dip</item>
+        <item name="android:paddingStart">4dip</item>
+        <item name="android:paddingEnd">28dip</item>
+    </style>
+
 </resources>
diff --git a/res/values/themes.xml b/res/values/themes.xml
index b8c7600..36adfbe 100644
--- a/res/values/themes.xml
+++ b/res/values/themes.xml
@@ -18,7 +18,8 @@
 <resources>
 
     <style name="Theme" parent="@android:style/Theme.Material.Light.DarkActionBar">
-        <item name="android:colorPrimary">@color/calculator_accent_color</item>
+        <item name="android:colorPrimary">@color/calculator_primary_color</item>
+        <item name="android:colorAccent">@color/calculator_accent_color</item>
         <item name="android:statusBarColor">@color/calculator_accent_color</item>
         <item name="android:windowSoftInputMode">stateAlwaysHidden</item>
     </style>
diff --git a/src/com/android/calculator2/AlertDialogFragment.java b/src/com/android/calculator2/AlertDialogFragment.java
index 47f482f..fdb7427 100644
--- a/src/com/android/calculator2/AlertDialogFragment.java
+++ b/src/com/android/calculator2/AlertDialogFragment.java
@@ -20,9 +20,11 @@
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.app.DialogFragment;
+import android.app.FragmentManager;
 import android.content.DialogInterface;
 import android.os.Bundle;
 import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
 import android.view.LayoutInflater;
 import android.widget.TextView;
 
@@ -47,16 +49,42 @@
     private static final String KEY_MESSAGE = NAME + "_message";
     private static final String KEY_BUTTON_NEGATIVE = NAME + "_button_negative";
     private static final String KEY_BUTTON_POSITIVE = NAME + "_button_positive";
+    private static final String KEY_TITLE = NAME + "_title";
+
+    /**
+     * Convenience method for creating and showing a DialogFragment with the given message and
+     * title.
+     *
+     * @param activity originating Activity
+     * @param title resource id for the title string
+     * @param message resource id for the displayed message string
+     * @param positiveButtonLabel label for second button, if any.  If non-null, activity must
+     * implement AlertDialogFragment.OnClickListener to respond.
+     */
+    public static void showMessageDialog(Activity activity, @StringRes int title,
+            @StringRes int message, @StringRes int positiveButtonLabel, @Nullable String tag) {
+        showMessageDialog(activity, title != 0 ? activity.getString(title) : null,
+                activity.getString(message),
+                positiveButtonLabel != 0 ? activity.getString(positiveButtonLabel) : null,
+                tag);
+    }
 
     /**
      * Create and show a DialogFragment with the given message.
+     *
      * @param activity originating Activity
+     * @param title displayed title, if any
      * @param message displayed message
      * @param positiveButtonLabel label for second button, if any.  If non-null, activity must
      * implement AlertDialogFragment.OnClickListener to respond.
      */
-    public static void showMessageDialog(Activity activity, CharSequence message,
-            @Nullable CharSequence positiveButtonLabel) {
+    public static void showMessageDialog(Activity activity, @Nullable CharSequence title,
+            CharSequence message, @Nullable CharSequence positiveButtonLabel, @Nullable String tag)
+    {
+        final FragmentManager manager = activity.getFragmentManager();
+        if (manager == null || manager.isDestroyed()) {
+            return;
+        }
         final AlertDialogFragment dialogFragment = new AlertDialogFragment();
         final Bundle args = new Bundle();
         args.putCharSequence(KEY_MESSAGE, message);
@@ -64,8 +92,9 @@
         if (positiveButtonLabel != null) {
             args.putCharSequence(KEY_BUTTON_POSITIVE, positiveButtonLabel);
         }
+        args.putCharSequence(KEY_TITLE, title);
         dialogFragment.setArguments(args);
-        dialogFragment.show(activity.getFragmentManager(), null /* tag */);
+        dialogFragment.show(manager, tag /* tag */);
     }
 
     public AlertDialogFragment() {
@@ -78,11 +107,11 @@
         final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
 
         final LayoutInflater inflater = LayoutInflater.from(builder.getContext());
-        final TextView textView = (TextView) inflater.inflate(
+        final TextView messageView = (TextView) inflater.inflate(
                 R.layout.dialog_message, null /* root */);
-        textView.setText(args.getCharSequence(KEY_MESSAGE));
+        messageView.setText(args.getCharSequence(KEY_MESSAGE));
+        builder.setView(messageView);
 
-        builder.setView(textView);
         builder.setNegativeButton(args.getCharSequence(KEY_BUTTON_NEGATIVE), null /* listener */);
 
         final CharSequence positiveButtonLabel = args.getCharSequence(KEY_BUTTON_POSITIVE);
@@ -90,6 +119,8 @@
             builder.setPositiveButton(positiveButtonLabel, this);
         }
 
+        builder.setTitle(args.getCharSequence(KEY_TITLE));
+
         return builder.create();
     }
 
diff --git a/src/com/android/calculator2/BoundedRational.java b/src/com/android/calculator2/BoundedRational.java
index e9e6f05..f3452a2 100644
--- a/src/com/android/calculator2/BoundedRational.java
+++ b/src/com/android/calculator2/BoundedRational.java
@@ -16,10 +16,10 @@
 
 package com.android.calculator2;
 
-import java.util.Random;
+import com.hp.creals.CR;
 
 import java.math.BigInteger;
-import com.hp.creals.CR;
+import java.util.Random;
 
 /**
  * Rational numbers that may turn to null if they get too big.
@@ -170,18 +170,19 @@
     static Random sReduceRng = new Random();
 
     /**
-     * Return a possibly reduced version of this that's not tooBig().
+     * Return a possibly reduced version of r that's not tooBig().
      * Return null if none exists.
      */
-    private BoundedRational maybeReduce() {
+    private static BoundedRational maybeReduce(BoundedRational r) {
+        if (r == null) return null;
         // Reduce randomly, with 1/16 probability, or if the result is too big.
-        if (!tooBig() && (sReduceRng.nextInt() & 0xf) != 0) {
-            return this;
+        if (!r.tooBig() && (sReduceRng.nextInt() & 0xf) != 0) {
+            return r;
         }
-        BoundedRational result = positiveDen();
+        BoundedRational result = r.positiveDen();
         result = result.reduce();
         if (!result.tooBig()) {
-            return this;
+            return result;
         }
         return null;
     }
@@ -225,7 +226,7 @@
         }
         final BigInteger den = r1.mDen.multiply(r2.mDen);
         final BigInteger num = r1.mNum.multiply(r2.mDen).add(r2.mNum.multiply(r1.mDen));
-        return new BoundedRational(num,den).maybeReduce();
+        return maybeReduce(new BoundedRational(num,den));
     }
 
     /**
@@ -239,7 +240,7 @@
         return new BoundedRational(r.mNum.negate(), r.mDen);
     }
 
-    static BoundedRational subtract(BoundedRational r1, BoundedRational r2) {
+    public static BoundedRational subtract(BoundedRational r1, BoundedRational r2) {
         return add(r1, negate(r2));
     }
 
@@ -264,8 +265,8 @@
         return new BoundedRational(num,den);
     }
 
-    static BoundedRational multiply(BoundedRational r1, BoundedRational r2) {
-        return rawMultiply(r1, r2).maybeReduce();
+    public static BoundedRational multiply(BoundedRational r1, BoundedRational r2) {
+        return maybeReduce(rawMultiply(r1, r2));
     }
 
     public static class ZeroDivisionException extends ArithmeticException {
@@ -277,7 +278,7 @@
     /**
      * Return the reciprocal of r (or null if the argument was null).
      */
-    static BoundedRational inverse(BoundedRational r) {
+    public static BoundedRational inverse(BoundedRational r) {
         if (r == null) {
             return null;
         }
@@ -287,11 +288,11 @@
         return new BoundedRational(r.mDen, r.mNum);
     }
 
-    static BoundedRational divide(BoundedRational r1, BoundedRational r2) {
+    public static BoundedRational divide(BoundedRational r1, BoundedRational r2) {
         return multiply(r1, inverse(r2));
     }
 
-    static BoundedRational sqrt(BoundedRational r) {
+    public static BoundedRational sqrt(BoundedRational r) {
         // Return non-null if numerator and denominator are small perfect squares.
         if (r == null) {
             return null;
@@ -395,7 +396,7 @@
      * Return Integer.MAX_VALUE if that's not possible.  Never returns a value less than zero, even
      * if r is a power of ten.
      */
-    static int digitsRequired(BoundedRational r) {
+    public static int digitsRequired(BoundedRational r) {
         if (r == null) {
             return Integer.MAX_VALUE;
         }
diff --git a/src/com/android/calculator2/Calculator.java b/src/com/android/calculator2/Calculator.java
index e2c16b3..b3d79eb 100644
--- a/src/com/android/calculator2/Calculator.java
+++ b/src/com/android/calculator2/Calculator.java
@@ -33,8 +33,10 @@
 import android.animation.PropertyValuesHolder;
 import android.app.ActionBar;
 import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
 import android.content.ClipData;
-import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.res.Resources;
@@ -43,6 +45,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
+import android.support.annotation.StringRes;
 import android.support.v4.content.ContextCompat;
 import android.support.v4.view.ViewPager;
 import android.text.Editable;
@@ -51,12 +54,14 @@
 import android.text.TextUtils;
 import android.text.TextWatcher;
 import android.text.style.ForegroundColorSpan;
+import android.util.Log;
 import android.util.Property;
 import android.view.ActionMode;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
 import android.view.Menu;
 import android.view.MenuItem;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnLongClickListener;
 import android.view.ViewAnimationUtils;
@@ -67,7 +72,7 @@
 import android.widget.TextView;
 import android.widget.Toolbar;
 
-import com.android.calculator2.CalculatorText.OnTextSizeChangeListener;
+import com.android.calculator2.CalculatorFormula.OnTextSizeChangeListener;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -76,11 +81,16 @@
 import java.io.ObjectInputStream;
 import java.io.ObjectOutput;
 import java.io.ObjectOutputStream;
+import java.text.DecimalFormatSymbols;
+
+import static com.android.calculator2.CalculatorFormula.OnFormulaContextMenuClickListener;
 
 public class Calculator extends Activity
-        implements OnTextSizeChangeListener, OnLongClickListener, CalculatorText.OnPasteListener,
-        AlertDialogFragment.OnClickListener {
+        implements OnTextSizeChangeListener, OnLongClickListener,
+        AlertDialogFragment.OnClickListener, Evaluator.EvaluationListener /* for main result */,
+        DragLayout.CloseCallback, DragLayout.DragCallback {
 
+    private static final String TAG = "Calculator";
     /**
      * Constant for an invalid resource id.
      */
@@ -93,10 +103,13 @@
                         // Not used for instant result evaluation.
         INIT,           // Very temporary state used as alternative to EVALUATE
                         // during reinitialization.  Do not animate on completion.
+        INIT_FOR_RESULT,  // Identical to INIT, but evaluation is known to terminate
+                          // with result, and current expression has been copied to history.
         ANIMATE,        // Result computed, animation to enlarge result window in progress.
         RESULT,         // Result displayed, formula invisible.
                         // If we are in RESULT state, the formula was evaluated without
                         // 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.
     }
@@ -107,11 +120,12 @@
     // initially evaluate assuming we were given a well-defined problem.  If we
     // were actually asked to compute sqrt(<extremely tiny negative number>) we produce 0
     // unless we are asked for enough precision that we can distinguish the argument from zero.
-    // TODO: Consider further heuristics to reduce the chance of observing this?
-    //       It already seems to be observable only in contrived cases.
-    // ANIMATE, ERROR, and RESULT are translated to an INIT state if the application
+    // ERROR and RESULT are translated to INIT or INIT_FOR_RESULT state if the application
     // is restarted in that state.  This leads us to recompute and redisplay the result
-    // ASAP.
+    // ASAP. We avoid saving the ANIMATE state or activating history in that state.
+    // In INIT_FOR_RESULT, and RESULT state, a copy of the current
+    // expression has been saved in the history db; in the other non-ANIMATE states,
+    // it has not.
     // TODO: Possibly save a bit more information, e.g. its initial display string
     // or most significant digit position, to speed up restart.
 
@@ -136,6 +150,10 @@
      */
     private static final String KEY_EVAL_STATE = NAME + "_eval_state";
     private static final String KEY_INVERSE_MODE = NAME + "_inverse_mode";
+    /**
+     * Associated value is an boolean holding the visibility state of the toolbar.
+     */
+    private static final String KEY_SHOW_TOOLBAR = NAME + "_show_toolbar";
 
     private final ViewTreeObserver.OnPreDrawListener mPreDrawListener =
             new ViewTreeObserver.OnPreDrawListener() {
@@ -150,6 +168,63 @@
         }
     };
 
+    private final Evaluator.Callback mEvaluatorCallback = new Evaluator.Callback() {
+        @Override
+        public void onMemoryStateChanged() {
+            mFormulaText.onMemoryStateChanged();
+        }
+
+        @Override
+        public void showMessageDialog(@StringRes int title, @StringRes int message,
+                @StringRes int positiveButtonLabel, String tag) {
+            AlertDialogFragment.showMessageDialog(Calculator.this, title, message,
+                    positiveButtonLabel, tag);
+
+        }
+    };
+
+    private final OnDisplayMemoryOperationsListener mOnDisplayMemoryOperationsListener =
+            new OnDisplayMemoryOperationsListener() {
+        @Override
+        public boolean shouldDisplayMemory() {
+            return mEvaluator.getMemoryIndex() != 0;
+        }
+    };
+
+    private final OnFormulaContextMenuClickListener mOnFormulaContextMenuClickListener =
+            new OnFormulaContextMenuClickListener() {
+        @Override
+        public boolean onPaste(ClipData clip) {
+            final ClipData.Item item = clip.getItemCount() == 0 ? null : clip.getItemAt(0);
+            if (item == null) {
+                // nothing to paste, bail early...
+                return false;
+            }
+
+            // 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)) {
+                clearIfNotInputState();
+                mEvaluator.appendExpr(mEvaluator.getSavedIndex());
+                redisplayAfterFormulaChange();
+            } else {
+                addChars(item.coerceToText(Calculator.this).toString(), false);
+            }
+            return true;
+        }
+
+        @Override
+        public void onMemoryRecall() {
+            clearIfNotInputState();
+            long memoryIndex = mEvaluator.getMemoryIndex();
+            if (memoryIndex != 0) {
+                mEvaluator.appendExpr(mEvaluator.getMemoryIndex());
+                redisplayAfterFormulaChange();
+            }
+        }
+    };
+
+
     private final TextWatcher mFormulaTextWatcher = new TextWatcher() {
         @Override
         public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
@@ -174,14 +249,16 @@
 
     private CalculatorDisplay mDisplayView;
     private TextView mModeView;
-    private CalculatorText mFormulaText;
+    private CalculatorFormula mFormulaText;
     private CalculatorResult mResultText;
     private HorizontalScrollView mFormulaContainer;
+    private DragLayout mDragLayout;
 
     private ViewPager mPadViewPager;
     private View mDeleteButton;
     private View mClearButton;
     private View mEqualButton;
+    private View mMainCalculator;
 
     private TextView mInverseToggle;
     private TextView mModeToggle;
@@ -201,12 +278,85 @@
     private ForegroundColorSpan mUnprocessedColorSpan = new ForegroundColorSpan(Color.RED);
 
     // Whether the display is one line.
-    private boolean mOneLine;
+    private boolean mIsOneLine;
+
+    /**
+     * Map the old saved state to a new state reflecting requested result reevaluation.
+     */
+    private CalculatorState mapFromSaved(CalculatorState savedState) {
+        switch (savedState) {
+            case RESULT:
+            case INIT_FOR_RESULT:
+                // Evaluation is expected to terminate normally.
+                return CalculatorState.INIT_FOR_RESULT;
+            case ERROR:
+            case INIT:
+                return CalculatorState.INIT;
+            case EVALUATE:
+            case INPUT:
+                return savedState;
+            default:  // Includes ANIMATE state.
+                throw new AssertionError("Impossible saved state");
+        }
+    }
+
+    /**
+     * Restore Evaluator state and mCurrentState from savedInstanceState.
+     * Return true if the toolbar should be visible.
+     */
+    private void restoreInstanceState(Bundle savedInstanceState) {
+        final CalculatorState savedState = CalculatorState.values()[
+                savedInstanceState.getInt(KEY_DISPLAY_STATE,
+                        CalculatorState.INPUT.ordinal())];
+        setState(savedState);
+        CharSequence unprocessed = savedInstanceState.getCharSequence(KEY_UNPROCESSED_CHARS);
+        if (unprocessed != null) {
+            mUnprocessedChars = unprocessed.toString();
+        }
+        byte[] state = savedInstanceState.getByteArray(KEY_EVAL_STATE);
+        if (state != null) {
+            try (ObjectInput in = new ObjectInputStream(new ByteArrayInputStream(state))) {
+                mEvaluator.restoreInstanceState(in);
+            } catch (Throwable ignored) {
+                // When in doubt, revert to clean state
+                mCurrentState = CalculatorState.INPUT;
+                mEvaluator.clearMain();
+            }
+        }
+        if (savedInstanceState.getBoolean(KEY_SHOW_TOOLBAR, true)) {
+            showAndMaybeHideToolbar();
+        } else {
+            mDisplayView.hideToolbar();
+        }
+        onInverseToggled(savedInstanceState.getBoolean(KEY_INVERSE_MODE));
+        // TODO: We're currently not saving and restoring scroll position.
+        //       We probably should.  Details may require care to deal with:
+        //         - new display size
+        //         - slow recomputation if we've scrolled far.
+    }
+
+    private void restoreDisplay() {
+        onModeChanged(mEvaluator.getDegreeMode(Evaluator.MAIN_INDEX));
+        if (mCurrentState != CalculatorState.RESULT
+            && mCurrentState != CalculatorState.INIT_FOR_RESULT) {
+            redisplayFormula();
+        }
+        if (mCurrentState == CalculatorState.INPUT) {
+            // This resultText will explicitly call evaluateAndNotify when ready.
+            mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_EVALUATE, this);
+        } else {
+            // Just reevaluate.
+            setState(mapFromSaved(mCurrentState));
+            // Request evaluation when we know display width.
+            mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_REQUIRE, this);
+        }
+    }
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        setContentView(R.layout.activity_calculator);
+
+        setContentView(R.layout.activity_calculator_main);
         setActionBar((Toolbar) findViewById(R.id.toolbar));
 
         // Hide all default options in the ActionBar.
@@ -220,24 +370,32 @@
             }
         });
 
+        mMainCalculator = findViewById(R.id.main_calculator);
         mDisplayView = (CalculatorDisplay) findViewById(R.id.display);
         mModeView = (TextView) findViewById(R.id.mode);
-        mFormulaText = (CalculatorText) findViewById(R.id.formula);
+        mFormulaText = (CalculatorFormula) findViewById(R.id.formula);
         mResultText = (CalculatorResult) findViewById(R.id.result);
         mFormulaContainer = (HorizontalScrollView) findViewById(R.id.formula_container);
+        mEvaluator = Evaluator.getInstance(this);
+        mEvaluator.setCallback(mEvaluatorCallback);
+        mResultText.setEvaluator(mEvaluator, Evaluator.MAIN_INDEX);
+        KeyMaps.setActivity(this);
 
         mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
         mDeleteButton = findViewById(R.id.del);
         mClearButton = findViewById(R.id.clr);
-        mEqualButton = findViewById(R.id.pad_numeric).findViewById(R.id.eq);
+        final View numberPad = findViewById(R.id.pad_numeric);
+        mEqualButton = numberPad.findViewById(R.id.eq);
         if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) {
             mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq);
         }
+        final TextView decimalPointButton = (TextView) numberPad.findViewById(R.id.dec_point);
+        decimalPointButton.setText(getDecimalSeparator());
 
         mInverseToggle = (TextView) findViewById(R.id.toggle_inv);
         mModeToggle = (TextView) findViewById(R.id.toggle_mode);
 
-        mOneLine = mResultText.getVisibility() == View.INVISIBLE;
+        mIsOneLine = mResultText.getVisibility() == View.INVISIBLE;
 
         mInvertibleButtons = new View[] {
                 findViewById(R.id.fun_sin),
@@ -256,62 +414,43 @@
                 findViewById(R.id.op_sqr)
         };
 
-        mEvaluator = new Evaluator(this, mResultText);
-        mResultText.setEvaluator(mEvaluator);
-        KeyMaps.setActivity(this);
+        mDragLayout = (DragLayout) findViewById(R.id.drag_layout);
+        mDragLayout.removeDragCallback(this);
+        mDragLayout.addDragCallback(this);
+        mDragLayout.setCloseCallback(this);
 
-        if (savedInstanceState != null) {
-            setState(CalculatorState.values()[
-                savedInstanceState.getInt(KEY_DISPLAY_STATE,
-                                          CalculatorState.INPUT.ordinal())]);
-            CharSequence unprocessed = savedInstanceState.getCharSequence(KEY_UNPROCESSED_CHARS);
-            if (unprocessed != null) {
-                mUnprocessedChars = unprocessed.toString();
-            }
-            byte[] state = savedInstanceState.getByteArray(KEY_EVAL_STATE);
-            if (state != null) {
-                try (ObjectInput in = new ObjectInputStream(new ByteArrayInputStream(state))) {
-                    mEvaluator.restoreInstanceState(in);
-                } catch (Throwable ignored) {
-                    // When in doubt, revert to clean state
-                    mCurrentState = CalculatorState.INPUT;
-                    mEvaluator.clear();
-                }
-            }
-        } else {
-            mCurrentState = CalculatorState.INPUT;
-            mEvaluator.clear();
-        }
+        mFormulaText.setOnContextMenuClickListener(mOnFormulaContextMenuClickListener);
+        mFormulaText.setOnDisplayMemoryOperationsListener(mOnDisplayMemoryOperationsListener);
 
         mFormulaText.setOnTextSizeChangeListener(this);
-        mFormulaText.setOnPasteListener(this);
         mFormulaText.addTextChangedListener(mFormulaTextWatcher);
         mDeleteButton.setOnLongClickListener(this);
 
-        onInverseToggled(savedInstanceState != null
-                && savedInstanceState.getBoolean(KEY_INVERSE_MODE));
-        onModeChanged(mEvaluator.getDegreeMode());
-
-        if (mCurrentState != CalculatorState.INPUT) {
-            // Just reevaluate.
-            redisplayFormula();
-            setState(CalculatorState.INIT);
-            mEvaluator.requireResult();
+        if (savedInstanceState != null) {
+            restoreInstanceState(savedInstanceState);
         } else {
-            redisplayAfterFormulaChange();
+            mCurrentState = CalculatorState.INPUT;
+            mEvaluator.clearMain();
+            showAndMaybeHideToolbar();
+            onInverseToggled(false);
         }
-        // TODO: We're currently not saving and restoring scroll position.
-        //       We probably should.  Details may require care to deal with:
-        //         - new display size
-        //         - slow recomputation if we've scrolled far.
+        restoreDisplay();
     }
 
     @Override
     protected void onResume() {
         super.onResume();
-
-        // Always temporarily show the toolbar initially on launch.
-        showAndMaybeHideToolbar();
+        if (mDisplayView.isToolbarVisible()) {
+            showAndMaybeHideToolbar();
+        }
+        // If HistoryFragment is showing, hide the main Calculator elements from accessibility.
+        // This is because Talkback does not use visibility as a cue for RelativeLayout elements,
+        // and RelativeLayout is the base class of DragLayout.
+        // If we did not do this, it would be possible to traverse to main Calculator elements from
+        // HistoryFragment.
+        mMainCalculator.setImportantForAccessibility(
+                mDragLayout.isOpen() ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+                        : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
     }
 
     @Override
@@ -334,6 +473,10 @@
         }
         outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray());
         outState.putBoolean(KEY_INVERSE_MODE, mInverseToggle.isSelected());
+        outState.putBoolean(KEY_SHOW_TOOLBAR, mDisplayView.isToolbarVisible());
+        // We must wait for asynchronous writes to complete, since outState may contain
+        // references to expressions being written.
+        mEvaluator.waitForWrites();
     }
 
     // Set the state, updating delete label and display colors.
@@ -342,6 +485,8 @@
     private void setState(CalculatorState state) {
         if (mCurrentState != state) {
             if (state == CalculatorState.INPUT) {
+                // We'll explicitly request evaluation from now on.
+                mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_NOT_EVALUATE, null);
                 restoreDisplayPositions();
             }
             mCurrentState = state;
@@ -355,7 +500,7 @@
                 mClearButton.setVisibility(View.GONE);
             }
 
-            if (mOneLine) {
+            if (mIsOneLine) {
                 if (mCurrentState == CalculatorState.RESULT
                         || mCurrentState == CalculatorState.EVALUATE
                         || mCurrentState == CalculatorState.ANIMATE) {
@@ -382,17 +527,43 @@
                 mResultText.setTextColor(
                         ContextCompat.getColor(this, R.color.display_result_text_color));
                 getWindow().setStatusBarColor(
-                        ContextCompat.getColor(this, R.color.calculator_accent_color));
+                        ContextCompat.getColor(this, R.color.calculator_statusbar_color));
             }
 
             invalidateOptionsMenu();
         }
     }
 
+    public boolean isResultLayout() {
+        if (mCurrentState == CalculatorState.ANIMATE) {
+            throw new AssertionError("impossible state");
+        }
+        // Note that ERROR has INPUT, not RESULT layout.
+        return mCurrentState == CalculatorState.INIT_FOR_RESULT
+                || mCurrentState == CalculatorState.RESULT;
+    }
+
+    public boolean isOneLine() {
+        return mIsOneLine;
+    }
+
+    @Override
+    protected void onDestroy() {
+        mDragLayout.removeDragCallback(this);
+        super.onDestroy();
+    }
+
+    /**
+     * Destroy the evaluator and close the underlying database.
+     */
+    public void destroyEvaluator() {
+        mEvaluator.destroyEvaluator();
+    }
+
     @Override
     public void onActionModeStarted(ActionMode mode) {
         super.onActionModeStarted(mode);
-        if (mode.getTag() == CalculatorText.TAG_ACTION_MODE) {
+        if (mode.getTag() == CalculatorFormula.TAG_ACTION_MODE) {
             mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
         }
     }
@@ -402,13 +573,8 @@
      * Return true if there was one.
      */
     private boolean stopActionModeOrContextMenu() {
-        if (mResultText.stopActionModeOrContextMenu()) {
-            return true;
-        }
-        if (mFormulaText.stopActionModeOrContextMenu()) {
-            return true;
-        }
-        return false;
+        return mResultText.stopActionModeOrContextMenu()
+                || mFormulaText.stopActionModeOrContextMenu();
     }
 
     @Override
@@ -423,8 +589,28 @@
     }
 
     @Override
+    public boolean dispatchTouchEvent(MotionEvent e) {
+        if (e.getActionMasked() == MotionEvent.ACTION_DOWN) {
+            stopActionModeOrContextMenu();
+
+            final HistoryFragment historyFragment = getHistoryFragment();
+            if (mDragLayout.isOpen() && historyFragment != null) {
+                historyFragment.stopActionModeOrContextMenu();
+            }
+        }
+        return super.dispatchTouchEvent(e);
+    }
+
+    @Override
     public void onBackPressed() {
         if (!stopActionModeOrContextMenu()) {
+            final HistoryFragment historyFragment = getHistoryFragment();
+            if (mDragLayout.isOpen() && historyFragment != null) {
+                if (!historyFragment.stopActionModeOrContextMenu()) {
+                    removeHistoryFragment();
+                }
+                return;
+            }
             if (mPadViewPager != null && mPadViewPager.getCurrentItem() != 0) {
                 // Select the previous pad.
                 mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
@@ -441,6 +627,7 @@
         // Allow the system to handle special key codes (e.g. "BACK" or "DPAD").
         switch (keyCode) {
             case KeyEvent.KEYCODE_BACK:
+            case KeyEvent.KEYCODE_ESCAPE:
             case KeyEvent.KEYCODE_DPAD_UP:
             case KeyEvent.KEYCODE_DPAD_DOWN:
             case KeyEvent.KEYCODE_DPAD_LEFT:
@@ -451,12 +638,10 @@
         // Stop the action mode or context menu if it's showing.
         stopActionModeOrContextMenu();
 
-        // Always cancel unrequested in-progress evaluation, so that we don't have to worry about
-        // subsequent asynchronous completion.
+        // Always cancel unrequested in-progress evaluation of the main expression, so that
+        // we don't have to worry about subsequent asynchronous completion.
         // Requested in-progress evaluations are handled below.
-        if (mCurrentState != CalculatorState.EVALUATE) {
-            mEvaluator.cancelAll(true);
-        }
+        cancelUnrequested();
 
         switch (keyCode) {
             case KeyEvent.KEYCODE_NUMPAD_ENTER:
@@ -469,6 +654,10 @@
                 mCurrentButton = mDeleteButton;
                 onDelete();
                 return true;
+            case KeyEvent.KEYCODE_CLEAR:
+                mCurrentButton = mClearButton;
+                onClear();
+                return true;
             default:
                 cancelIfEvaluating(false);
                 final int raw = event.getKeyCharacterMap().get(keyCode, event.getMetaState());
@@ -519,7 +708,8 @@
     }
 
     /**
-     * Invoked whenever the deg/rad mode may have changed to update the UI.
+     * Invoked whenever the deg/rad mode may have changed to update the UI. Note that the mode has
+     * not necessarily actually changed where this is invoked.
      *
      * @param degreeMode {@code true} if in degree mode
      */
@@ -537,9 +727,16 @@
             mModeToggle.setText(R.string.mode_deg);
             mModeToggle.setContentDescription(getString(R.string.desc_switch_deg));
         }
+    }
 
-        // Show the toolbar to highlight the mode change.
-        showAndMaybeHideToolbar();
+    private void removeHistoryFragment() {
+        final FragmentManager manager = getFragmentManager();
+        if (manager != null && !manager.isDestroyed()) {
+            manager.popBackStack(HistoryFragment.TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE);
+        }
+
+        // When HistoryFragment is hidden, the main Calculator is important for accessibility again.
+        mMainCalculator.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
     }
 
     /**
@@ -548,10 +745,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);
     }
@@ -576,25 +773,28 @@
      */
     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);
     }
 
+    public void evaluateInstantIfNecessary() {
+        if (mCurrentState == CalculatorState.INPUT
+                && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
+            mEvaluator.evaluateAndNotify(Evaluator.MAIN_INDEX, this, mResultText);
+        }
+    }
+
     private void redisplayAfterFormulaChange() {
         // 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();
-            }
+            evaluateInstantIfNecessary();
         }
     }
 
@@ -627,10 +827,7 @@
         stopActionModeOrContextMenu();
 
         // See onKey above for the rationale behind some of the behavior below:
-        if (mCurrentState != CalculatorState.EVALUATE) {
-            // Cancel evaluations that were not specifically requested.
-            mEvaluator.cancelAll(true);
-        }
+        cancelUnrequested();
 
         final int id = view.getId();
         switch (id) {
@@ -653,20 +850,24 @@
                 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.
                 mEvaluator.setDegreeMode(mode);
                 onModeChanged(mode);
+                // Show the toolbar to highlight the mode change.
+                showAndMaybeHideToolbar();
                 setState(CalculatorState.INPUT);
                 mResultText.clear();
-                if (!haveUnprocessed() && mEvaluator.getExpr().hasInterestingOps()) {
-                    mEvaluator.evaluateAndShowResult();
+                if (!haveUnprocessed()) {
+                    evaluateInstantIfNecessary();
                 }
-                return;  // onModeChanged adjusted toolbar visibility.
+                return;
             default:
                 cancelIfEvaluating(false);
                 if (haveUnprocessed()) {
@@ -683,7 +884,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,
@@ -706,28 +908,35 @@
     }
 
     // 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");
+        }
+
         // Invalidate any options that may depend on the current result.
         invalidateOptionsMenu();
 
-        mResultText.displayResult(initDisplayPrec, msd, leastDigPos, truncatedWholeNumber);
-        if (mCurrentState != CalculatorState.INPUT) { // in EVALUATE or INIT state
-            onResult(mCurrentState != CalculatorState.INIT);
+        mResultText.onEvaluate(index, initDisplayPrec, msd, leastDigPos, truncatedWholeNumber);
+        if (mCurrentState != CalculatorState.INPUT) {
+            // In EVALUATE, INIT, RESULT, or INIT_FOR_RESULT state.
+            onResult(mCurrentState == CalculatorState.EVALUATE /* animate */,
+                     mCurrentState == CalculatorState.INIT_FOR_RESULT
+                    || mCurrentState == CalculatorState.RESULT /* previously preserved */);
         }
     }
 
     // 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()
-    {
-        mResultText.redisplay();
+    public void onReevaluate(long index) {
+        // Index is Evaluator.MAIN_INDEX.
+        mResultText.onReevaluate(index);
     }
 
     @Override
@@ -764,13 +973,20 @@
      */
     private boolean cancelIfEvaluating(boolean quiet) {
         if (mCurrentState == CalculatorState.EVALUATE) {
-            mEvaluator.cancelAll(quiet);
+            mEvaluator.cancel(Evaluator.MAIN_INDEX, quiet);
             return true;
         } else {
             return false;
         }
     }
 
+
+    private void cancelUnrequested() {
+        if (mCurrentState == CalculatorState.INPUT) {
+            mEvaluator.cancel(Evaluator.MAIN_INDEX, true);
+        }
+    }
+
     private boolean haveUnprocessed() {
         return mUnprocessedChars != null && !mUnprocessedChars.isEmpty();
     }
@@ -780,10 +996,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);
             }
         }
     }
@@ -802,7 +1018,7 @@
         } 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();
         }
@@ -867,27 +1083,35 @@
         mResultText.announceForAccessibility(getResources().getString(R.string.cleared));
     }
 
+    public void onClearAnimationEnd() {
+         mUnprocessedChars = null;
+         mResultText.clear();
+         mEvaluator.clearMain();
+         setState(CalculatorState.INPUT);
+         redisplayFormula();
+    }
+
     private void onClear() {
-        if (mEvaluator.getExpr().isEmpty() && !haveUnprocessed()) {
+        if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).isEmpty() && !haveUnprocessed()) {
             return;
         }
         cancelIfEvaluating(true);
         announceClearedForAccessibility();
-        reveal(mCurrentButton, R.color.calculator_accent_color, new AnimatorListenerAdapter() {
+        reveal(mCurrentButton, R.color.calculator_primary_color, new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
-                mUnprocessedChars = null;
-                mResultText.clear();
-                mEvaluator.clear();
-                setState(CalculatorState.INPUT);
+                onClearAnimationEnd();
                 showOrHideToolbar();
-                redisplayFormula();
             }
         });
     }
 
     // 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));
@@ -896,12 +1120,13 @@
                         @Override
                         public void onAnimationEnd(Animator animation) {
                            setState(CalculatorState.ERROR);
-                           mResultText.displayError(errorResourceId);
+                           mResultText.onError(index, errorResourceId);
                         }
                     });
-        } else if (mCurrentState == CalculatorState.INIT) {
+        } else if (mCurrentState == CalculatorState.INIT
+                || mCurrentState == CalculatorState.INIT_FOR_RESULT /* very unlikely */) {
             setState(CalculatorState.ERROR);
-            mResultText.displayError(errorResourceId);
+            mResultText.onError(index, errorResourceId);
         } else {
             mResultText.clear();
         }
@@ -914,7 +1139,7 @@
     // formula and result displays back at the end of the animation.  We no longer do that,
     // 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) {
+    private void onResult(boolean animate, boolean resultWasPreserved) {
         // Calculate the textSize that would be used to display the result in the formula.
         // For scrollable results just use the minimum textSize to maximize the number of digits
         // that are visible on screen.
@@ -936,7 +1161,7 @@
         final float resultTranslationY = (mFormulaContainer.getBottom() - mResultText.getBottom())
                 - (mFormulaText.getPaddingBottom() - mResultText.getPaddingBottom());
         float formulaTranslationY = -mFormulaContainer.getBottom();
-        if (mOneLine) {
+        if (mIsOneLine) {
             // Position the result text.
             mResultText.setY(mResultText.getBottom());
             formulaTranslationY = -(findViewById(R.id.toolbar).getBottom()
@@ -946,6 +1171,14 @@
         // Change the result's textColor to match the formula.
         final int formulaTextColor = mFormulaText.getCurrentTextColor();
 
+        if (resultWasPreserved) {
+            // Result was previously addded to history.
+            mEvaluator.represerve();
+        } else {
+            // Add current result to history.
+            mEvaluator.preserve(Evaluator.MAIN_INDEX, true);
+        }
+
         if (animate) {
             mResultText.announceForAccessibility(getResources().getString(R.string.desc_eq));
             mResultText.announceForAccessibility(mResultText.getText());
@@ -971,7 +1204,7 @@
 
             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);
@@ -999,8 +1232,21 @@
     @Override
     public void onClick(AlertDialogFragment fragment, int which) {
         if (which == DialogInterface.BUTTON_POSITIVE) {
-            // Timeout extension request.
-            mEvaluator.setLongTimeOut();
+            if (HistoryFragment.CLEAR_DIALOG_TAG.equals(fragment.getTag())) {
+                // TODO: Try to preserve the current, saved, and memory expressions. How should we
+                // handle expressions to which they refer?
+                mEvaluator.clearEverything();
+                // TODO: It's not clear what we should really do here. This is an initial hack.
+                // May want to make onClearAnimationEnd() private if/when we fix this.
+                onClearAnimationEnd();
+                mEvaluatorCallback.onMemoryStateChanged();
+                onBackPressed();
+            } else if (Evaluator.TIMEOUT_DIALOG_TAG.equals(fragment.getTag())) {
+                // Timeout extension request.
+                mEvaluator.setLongTimeout();
+            } else {
+                Log.e(TAG, "Unknown AlertDialogFragment click:" + fragment.getTag());
+            }
         }
     }
 
@@ -1020,8 +1266,12 @@
         menu.findItem(R.id.menu_leading).setVisible(mCurrentState == CalculatorState.RESULT);
 
         // Show the fraction option when displaying a rational result.
-        menu.findItem(R.id.menu_fraction).setVisible(mCurrentState == CalculatorState.RESULT
-                && mEvaluator.getResult().exactlyDisplayable());
+        boolean visible = mCurrentState == CalculatorState.RESULT;
+        final UnifiedReal mainResult = mEvaluator.getResult(Evaluator.MAIN_INDEX);
+        // mainResult should never be null, but it happens. Check as a workaround to protect
+        // against crashes until we find the root cause (b/34763650).
+        visible &= mainResult != null && mainResult.exactlyDisplayable();
+        menu.findItem(R.id.menu_fraction).setVisible(visible);
 
         return true;
     }
@@ -1029,6 +1279,9 @@
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         switch (item.getItemId()) {
+            case R.id.menu_history:
+                showHistoryFragment();
+                return true;
             case R.id.menu_leading:
                 displayFull();
                 return true;
@@ -1043,13 +1296,106 @@
         }
     }
 
-    private void displayMessage(String s) {
-        AlertDialogFragment.showMessageDialog(this, s, null);
+    /* Begin override CloseCallback method. */
+
+    @Override
+    public void onClose() {
+        removeHistoryFragment();
+    }
+
+    /* End override CloseCallback method. */
+
+    /* Begin override DragCallback methods */
+
+    public void onStartDraggingOpen() {
+        mDisplayView.hideToolbar();
+        showHistoryFragment();
+    }
+
+    @Override
+    public void onInstanceStateRestored(boolean isOpen) {
+    }
+
+    @Override
+    public void whileDragging(float yFraction) {
+    }
+
+    @Override
+    public boolean shouldCaptureView(View view, int x, int y) {
+        return view.getId() == R.id.history_frame
+            && (mDragLayout.isMoving() || mDragLayout.isViewUnder(view, x, y));
+    }
+
+    @Override
+    public int getDisplayHeight() {
+        return mDisplayView.getMeasuredHeight();
+    }
+
+    /* End override DragCallback methods */
+
+    /**
+     * Change evaluation state to one that's friendly to the history fragment.
+     * Return false if that was not easily possible.
+     */
+    private boolean prepareForHistory() {
+        if (mCurrentState == CalculatorState.ANIMATE) {
+            throw new AssertionError("onUserInteraction should have ended animation");
+        } else if (mCurrentState == CalculatorState.EVALUATE) {
+            // Cancel current evaluation
+            cancelIfEvaluating(true /* quiet */ );
+            setState(CalculatorState.INPUT);
+            return true;
+        } else if (mCurrentState == CalculatorState.INIT) {
+            // Easiest to just refuse.  Otherwise we can see a state change
+            // while in history mode, which causes all sorts of problems.
+            // TODO: Consider other alternatives. If we're just doing the decimal conversion
+            // at the end of an evaluation, we could treat this as RESULT state.
+            return false;
+        }
+        // We should be in INPUT, INIT_FOR_RESULT, RESULT, or ERROR state.
+        return true;
+    }
+
+    private HistoryFragment getHistoryFragment() {
+        final FragmentManager manager = getFragmentManager();
+        if (manager == null || manager.isDestroyed()) {
+            return null;
+        }
+        final Fragment fragment = manager.findFragmentByTag(HistoryFragment.TAG);
+        return fragment == null || fragment.isRemoving() ? null : (HistoryFragment) fragment;
+    }
+
+    private void showHistoryFragment() {
+        final FragmentManager manager = getFragmentManager();
+        if (manager == null || manager.isDestroyed()) {
+            return;
+        }
+
+        if (getHistoryFragment() != null || !prepareForHistory()) {
+            return;
+        }
+
+        stopActionModeOrContextMenu();
+        manager.beginTransaction()
+                .replace(R.id.history_frame, new HistoryFragment(), HistoryFragment.TAG)
+                .setTransition(FragmentTransaction.TRANSIT_NONE)
+                .addToBackStack(HistoryFragment.TAG)
+                .commit();
+
+        // When HistoryFragment is visible, hide all descendants of the main Calculator view.
+        mMainCalculator.setImportantForAccessibility(
+                View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
+        // TODO: pass current scroll position of result
+    }
+
+    private void displayMessage(String title, String message) {
+        AlertDialogFragment.showMessageDialog(this, title, message, null, null /* tag */);
     }
 
     private void displayFraction() {
-        UnifiedReal result = mEvaluator.getResult();
-        displayMessage(KeyMaps.translateResult(result.toNiceString()));
+        UnifiedReal result = mEvaluator.getResult(Evaluator.MAIN_INDEX);
+        displayMessage(getString(R.string.menu_fraction),
+                KeyMaps.translateResult(result.toNiceString()));
     }
 
     // Display full result to currently evaluated precision
@@ -1061,7 +1407,7 @@
         } else {
             msg += res.getString(R.string.approximate);
         }
-        displayMessage(msg);
+        displayMessage(getString(R.string.menu_leading), msg);
     }
 
     /**
@@ -1105,7 +1451,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.
@@ -1154,28 +1500,21 @@
         showOrHideToolbar();
     }
 
-    @Override
-    public boolean onPaste(ClipData clip) {
-        final ClipData.Item item = clip.getItemCount() == 0 ? null : clip.getItemAt(0);
-        if (item == null) {
-            // nothing to paste, bail early...
-            return false;
+    private void clearIfNotInputState() {
+        if (mCurrentState == CalculatorState.ERROR
+                || mCurrentState == CalculatorState.RESULT) {
+            setState(CalculatorState.INPUT);
+            mEvaluator.clearMain();
         }
+    }
 
-        // 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();
-            redisplayAfterFormulaChange();
-        } else {
-            addChars(item.coerceToText(this).toString(), false);
-        }
-        return true;
+    /**
+     * Since we only support LTR format, using the RTL comma does not make sense.
+     */
+    private String getDecimalSeparator() {
+        final char defaultSeparator = DecimalFormatSymbols.getInstance().getDecimalSeparator();
+        final char rtlComma = '\u066b';
+        return defaultSeparator == rtlComma ? "," : String.valueOf(defaultSeparator);
     }
 
     /**
@@ -1185,4 +1524,8 @@
     public void onContextMenuClosed(Menu menu) {
         stopActionModeOrContextMenu();
     }
+
+    public interface OnDisplayMemoryOperationsListener {
+        boolean shouldDisplayMemory();
+    }
 }
diff --git a/src/com/android/calculator2/CalculatorDisplay.java b/src/com/android/calculator2/CalculatorDisplay.java
index 728fc11..341564d 100644
--- a/src/com/android/calculator2/CalculatorDisplay.java
+++ b/src/com/android/calculator2/CalculatorDisplay.java
@@ -191,7 +191,12 @@
      */
     public void hideToolbar() {
         if (!getForceToolbarVisible()) {
-            post(mHideToolbarRunnable);
+            removeCallbacks(mHideToolbarRunnable);
+            mHideToolbarRunnable.run();
         }
     }
+
+    public boolean isToolbarVisible() {
+        return mToolbar.getVisibility() == View.VISIBLE;
+    }
 }
diff --git a/src/com/android/calculator2/CalculatorExpr.java b/src/com/android/calculator2/CalculatorExpr.java
index 41dfe13..75ab1c9 100644
--- a/src/com/android/calculator2/CalculatorExpr.java
+++ b/src/com/android/calculator2/CalculatorExpr.java
@@ -21,16 +21,16 @@
 import android.text.SpannableStringBuilder;
 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.ByteArrayOutputStream;
 import java.io.DataInput;
 import java.io.DataOutput;
+import java.io.DataOutputStream;
 import java.io.IOException;
+import java.math.BigInteger;
 import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.IdentityHashMap;
+import java.util.Collections;
+import java.util.HashSet;
 
 /**
  * A mathematical expression represented as a sequence of "tokens".
@@ -47,6 +47,33 @@
  * 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);
+        /*
+         * Retrieve 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);
+    }
+
     private ArrayList<Token> mExpr;  // The actual representation
                                      // as a list of tokens.  Constant
                                      // tokens are always nonempty.
@@ -60,7 +87,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 +106,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 +143,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 +311,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 +366,27 @@
      * 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:
+                PreEval pe = new PreEval(in);
+                if (pe.mIndex == -1) {
+                    // Database corrupted by earlier bug.
+                    // Return a conspicuously wrong placeholder that won't lead to a crash.
+                    Constant result = new Constant();
+                    result.add(R.id.dec_point);
+                    return result;
+                } else {
+                    return pe;
+                }
+            default: throw new IOException("Bad save file format");
+            }
+        } else {
+            return new Operator(kindByte);
         }
     }
 
@@ -426,6 +421,21 @@
     }
 
     /**
+     * Use write() above to generate a byte array containing a serialized representation of
+     * this expression.
+     */
+    public byte[] toBytes() {
+        ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
+        try (DataOutputStream out = new DataOutputStream(byteArrayStream)) {
+            write(out);
+        } catch (IOException e) {
+            // Impossible; No IO involved.
+            throw new AssertionError("Impossible IO exception", e);
+        }
+        return byteArrayStream.toByteArray();
+    }
+
+    /**
      * Does this expression end with a numeric constant?
      * As opposed to an operator or preevaluated expression.
      */
@@ -441,7 +451,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);
@@ -591,7 +601,7 @@
      */
     public Object clone() {
         CalculatorExpr result = new CalculatorExpr();
-        for (Token t: mExpr) {
+        for (Token t : mExpr) {
             if (t instanceof Constant) {
                 result.mExpr.add((Token)(((Constant)t).clone()));
             } else {
@@ -612,14 +622,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 +653,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 +726,14 @@
             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) {
+                // We try to minimize this recursive evaluation case, but currently don't
+                // completely avoid it.
+                res = nestedEval(index, ec.mExprResolver);
+            }
+            return new EvalRet(i+1, res);
         }
         EvalRet argVal;
         switch(((Operator)(t)).id) {
@@ -968,7 +986,7 @@
      * Is the current expression worth evaluating?
      */
     public boolean hasInterestingOps() {
-        int last = trailingBinaryOpsStart();
+        final int last = trailingBinaryOpsStart();
         int first = 0;
         if (last > first && isOperatorUnchecked(first, R.id.op_sub)) {
             // Leading minus is not by itself interesting.
@@ -988,7 +1006,7 @@
      * Does the expression contain trig operations?
      */
     public boolean hasTrigFuncs() {
-        for (Token t: mExpr) {
+        for (Token t : mExpr) {
             if (t instanceof Operator) {
                 Operator o = (Operator)t;
                 if (KeyMaps.isTrigFunc(o.id)) {
@@ -1000,6 +1018,58 @@
     }
 
     /**
+     * Add the indices of unevaluated PreEval expressions embedded in the current expression to
+     * argument.  This includes only directly referenced expressions e, not those indirectly
+     * referenced by e. If the index was already present, it is not added. If the argument
+     * contained no duplicates, the result will not either. New indices are added to the end of
+     * the list.
+     */
+    private void addReferencedExprs(ArrayList<Long> list, ExprResolver er) {
+        for (Token t : mExpr) {
+            if (t instanceof PreEval) {
+                Long index = ((PreEval) t).mIndex;
+                if (er.getResult(index) == null && !list.contains(index)) {
+                    list.add(index);
+                }
+            }
+        }
+    }
+
+    /**
+     * Return a list of unevaluated expressions transitively referenced by the current one.
+     * All expressions in the resulting list will have had er.getExpr() called on them.
+     * The resulting list is ordered such that evaluating expressions in list order
+     * should trigger few recursive evaluations.
+     */
+    public ArrayList<Long> getTransitivelyReferencedExprs(ExprResolver er) {
+        // We could avoid triggering any recursive evaluations by actually building the
+        // dependency graph and topologically sorting it. Note that sorting by index works
+        // for positive and negative indices separately, but not their union. Currently we
+        // just settle for reverse breadth-first-search order, which handles the common case
+        // of simple dependency chains well.
+        ArrayList<Long> list = new ArrayList<Long>();
+        int scanned = 0;  // We've added expressions referenced by [0, scanned) to the list
+        addReferencedExprs(list, er);
+        while (scanned != list.size()) {
+            er.getExpr(list.get(scanned++)).addReferencedExprs(list, er);
+        }
+        Collections.reverse(list);
+        return list;
+    }
+
+    /**
+     * Evaluate the expression at the given index to a UnifiedReal.
+     * Both saves and returns the result.
+     */
+    UnifiedReal nestedEval(long index, ExprResolver er) throws SyntaxException {
+        CalculatorExpr nestedExpr = er.getExpr(index);
+        EvalContext newEc = new EvalContext(er.getDegreeMode(index),
+                nestedExpr.trailingBinaryOpsStart(), er);
+        EvalRet new_res = nestedExpr.evalExpr(0, newEc);
+        return er.putResultIfAbsent(index, new_res.val);
+    }
+
+    /**
      * Evaluate the expression excluding trailing binary operators.
      * Errors result in exceptions, most of which are unchecked.  Should not be called
      * concurrently with modification of the expression.  May take a very long time; avoid calling
@@ -1007,17 +1077,26 @@
      *
      * @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.
     {
+        // First evaluate all indirectly referenced expressions in increasing index order.
+        // This ensures that subsequent evaluation never encounters an embedded PreEval
+        // expression that has not been previously evaluated.
+        // We could do the embedded evaluations recursively, but that risks running out of
+        // stack space.
+        ArrayList<Long> referenced = getTransitivelyReferencedExprs(er);
+        for (long index : referenced) {
+            nestedEval(index, er);
+        }
         try {
             // We currently never include trailing binary operators, but include other trailing
             // operators.  Thus we usually, but not always, display results for prefixes of valid
             // 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");
@@ -1031,7 +1110,7 @@
     // Produce a string representation of the expression itself
     SpannableStringBuilder toSpannableStringBuilder(Context context) {
         SpannableStringBuilder ssb = new SpannableStringBuilder();
-        for (Token t: mExpr) {
+        for (Token t : mExpr) {
             ssb.append(t.toCharSequence(context));
         }
         return ssb;
diff --git a/src/com/android/calculator2/CalculatorText.java b/src/com/android/calculator2/CalculatorFormula.java
similarity index 70%
rename from src/com/android/calculator2/CalculatorText.java
rename to src/com/android/calculator2/CalculatorFormula.java
index de2a843..2911df8 100644
--- a/src/com/android/calculator2/CalculatorText.java
+++ b/src/com/android/calculator2/CalculatorFormula.java
@@ -25,7 +25,9 @@
 import android.os.Build;
 import android.text.Layout;
 import android.text.TextPaint;
+import android.text.TextUtils;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.util.TypedValue;
 import android.view.ActionMode;
 import android.view.ContextMenu;
@@ -36,9 +38,10 @@
 import android.widget.TextView;
 
 /**
- * TextView adapted for Calculator display.
+ * TextView adapted for displaying the formula and allowing pasting.
  */
-public class CalculatorText extends AlignedTextView implements MenuItem.OnMenuItemClickListener {
+public class CalculatorFormula extends AlignedTextView implements MenuItem.OnMenuItemClickListener,
+        ClipboardManager.OnPrimaryClipChangedListener {
 
     public static final String TAG_ACTION_MODE = "ACTION_MODE";
 
@@ -49,31 +52,36 @@
     private final float mMinimumTextSize;
     private final float mStepTextSize;
 
+    private final ClipboardManager mClipboardManager;
+
     private int mWidthConstraint = -1;
     private ActionMode mActionMode;
     private ActionMode.Callback mPasteActionModeCallback;
     private ContextMenu mContextMenu;
-    private OnPasteListener mOnPasteListener;
     private OnTextSizeChangeListener mOnTextSizeChangeListener;
+    private OnFormulaContextMenuClickListener mOnContextMenuClickListener;
+    private Calculator.OnDisplayMemoryOperationsListener mOnDisplayMemoryOperationsListener;
 
-    public CalculatorText(Context context) {
+    public CalculatorFormula(Context context) {
         this(context, null /* attrs */);
     }
 
-    public CalculatorText(Context context, AttributeSet attrs) {
+    public CalculatorFormula(Context context, AttributeSet attrs) {
         this(context, attrs, 0 /* defStyleAttr */);
     }
 
-    public CalculatorText(Context context, AttributeSet attrs, int defStyleAttr) {
+    public CalculatorFormula(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
 
+        mClipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+
         final TypedArray a = context.obtainStyledAttributes(
-                attrs, R.styleable.CalculatorText, defStyleAttr, 0);
+                attrs, R.styleable.CalculatorFormula, defStyleAttr, 0);
         mMaximumTextSize = a.getDimension(
-                R.styleable.CalculatorText_maxTextSize, getTextSize());
+                R.styleable.CalculatorFormula_maxTextSize, getTextSize());
         mMinimumTextSize = a.getDimension(
-                R.styleable.CalculatorText_minTextSize, getTextSize());
-        mStepTextSize = a.getDimension(R.styleable.CalculatorText_stepTextSize,
+                R.styleable.CalculatorFormula_minTextSize, getTextSize());
+        mStepTextSize = a.getDimension(R.styleable.CalculatorFormula_stepTextSize,
                 (mMaximumTextSize - mMinimumTextSize) / 3);
         a.recycle();
 
@@ -112,6 +120,21 @@
     }
 
     @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        mClipboardManager.addPrimaryClipChangedListener(this);
+        onPrimaryClipChanged();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        mClipboardManager.removePrimaryClipChangedListener(this);
+    }
+
+    @Override
     protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
         super.onTextChanged(text, start, lengthBefore, lengthAfter);
 
@@ -161,20 +184,6 @@
         return lastFitTextSize;
     }
 
-    private static boolean startsWith(CharSequence whole, CharSequence prefix) {
-        int wholeLen = whole.length();
-        int prefixLen = prefix.length();
-        if (prefixLen > wholeLen) {
-            return false;
-        }
-        for (int i = 0; i < prefixLen; ++i) {
-            if (prefix.charAt(i) != whole.charAt(i)) {
-                return false;
-            }
-        }
-        return true;
-    }
-
     /**
      * Functionally equivalent to setText(), but explicitly announce changes.
      * If the new text is an extension of the old one, announce the addition.
@@ -221,8 +230,13 @@
         mOnTextSizeChangeListener = listener;
     }
 
-    public void setOnPasteListener(OnPasteListener listener) {
-        mOnPasteListener = listener;
+    public void setOnContextMenuClickListener(OnFormulaContextMenuClickListener listener) {
+        mOnContextMenuClickListener = listener;
+    }
+
+    public void setOnDisplayMemoryOperationsListener(
+            Calculator.OnDisplayMemoryOperationsListener listener) {
+        mOnDisplayMemoryOperationsListener = listener;
     }
 
     /**
@@ -246,7 +260,7 @@
             public boolean onCreateActionMode(ActionMode mode, Menu menu) {
                 mode.setTag(TAG_ACTION_MODE);
                 final MenuInflater inflater = mode.getMenuInflater();
-                return createPasteMenu(inflater, menu);
+                return createContextMenu(inflater, menu);
             }
 
             @Override
@@ -265,8 +279,8 @@
                 outRect.top += getTotalPaddingTop();
                 outRect.right -= getTotalPaddingRight();
                 outRect.bottom -= getTotalPaddingBottom();
-                // Encourage menu positioning towards the right, possibly over formula.
-                outRect.left = outRect.right;
+                // Encourage menu positioning over the rightmost 10% of the screen.
+                outRect.left = (int) (outRect.right * 0.9f);
             }
         };
         setOnLongClickListener(new View.OnLongClickListener() {
@@ -287,10 +301,10 @@
             public void onCreateContextMenu(ContextMenu contextMenu, View view,
                     ContextMenu.ContextMenuInfo contextMenuInfo) {
                 final MenuInflater inflater = new MenuInflater(getContext());
-                createPasteMenu(inflater, contextMenu);
+                createContextMenu(inflater, contextMenu);
                 mContextMenu = contextMenu;
-                for(int i = 0; i < contextMenu.size(); i++) {
-                    contextMenu.getItem(i).setOnMenuItemClickListener(CalculatorText.this);
+                for (int i = 0; i < contextMenu.size(); i++) {
+                    contextMenu.getItem(i).setOnMenuItemClickListener(CalculatorFormula.this);
                 }
             }
         });
@@ -302,41 +316,77 @@
         });
     }
 
-    private boolean createPasteMenu(MenuInflater inflater, Menu menu) {
-        final ClipboardManager clipboard = (ClipboardManager) getContext()
-                .getSystemService(Context.CLIPBOARD_SERVICE);
-        if (clipboard.hasPrimaryClip()) {
-            bringPointIntoView(length());
-            inflater.inflate(R.menu.paste, menu);
-            return true;
+    private boolean createContextMenu(MenuInflater inflater, Menu menu) {
+        final boolean isPasteEnabled = isPasteEnabled();
+        final boolean isMemoryEnabled = isMemoryEnabled();
+        if (!isPasteEnabled && !isMemoryEnabled) {
+            return false;
         }
-        // Prevents the selection action mode on double tap.
-        return false;
+
+        bringPointIntoView(length());
+        inflater.inflate(R.menu.menu_formula, menu);
+        final MenuItem pasteItem = menu.findItem(R.id.menu_paste);
+        final MenuItem memoryRecallItem = menu.findItem(R.id.memory_recall);
+        pasteItem.setEnabled(isPasteEnabled);
+        memoryRecallItem.setEnabled(isMemoryEnabled);
+        return true;
     }
 
     private void paste() {
-        final ClipboardManager clipboard = (ClipboardManager) getContext()
-                .getSystemService(Context.CLIPBOARD_SERVICE);
-        final ClipData primaryClip = clipboard.getPrimaryClip();
-        if (primaryClip != null && mOnPasteListener != null) {
-            mOnPasteListener.onPaste(primaryClip);
+        final ClipData primaryClip = mClipboardManager.getPrimaryClip();
+        if (primaryClip != null && mOnContextMenuClickListener != null) {
+            mOnContextMenuClickListener.onPaste(primaryClip);
         }
     }
 
     @Override
     public boolean onMenuItemClick(MenuItem item) {
-        if (item.getItemId() == R.id.menu_paste) {
-            paste();
-            return true;
+        switch (item.getItemId()) {
+            case R.id.memory_recall:
+                mOnContextMenuClickListener.onMemoryRecall();
+                return true;
+            case R.id.menu_paste:
+                paste();
+                return true;
+            default:
+                return false;
         }
-        return false;
+    }
+
+    @Override
+    public void onPrimaryClipChanged() {
+        setLongClickable(isPasteEnabled() || isMemoryEnabled());
+    }
+
+    public void onMemoryStateChanged() {
+        setLongClickable(isPasteEnabled() || isMemoryEnabled());
+    }
+
+    private boolean isMemoryEnabled() {
+        return mOnDisplayMemoryOperationsListener != null
+                && mOnDisplayMemoryOperationsListener.shouldDisplayMemory();
+    }
+
+    private boolean isPasteEnabled() {
+        final ClipData clip = mClipboardManager.getPrimaryClip();
+        if (clip == null || clip.getItemCount() == 0) {
+            return false;
+        }
+        CharSequence clipText = null;
+        try {
+            clipText = clip.getItemAt(0).coerceToText(getContext());
+        } catch (Exception e) {
+            Log.i("Calculator", "Error reading clipboard:", e);
+        }
+        return !TextUtils.isEmpty(clipText);
     }
 
     public interface OnTextSizeChangeListener {
         void onTextSizeChanged(TextView textView, float oldSize);
     }
 
-    public interface OnPasteListener {
+    public interface OnFormulaContextMenuClickListener {
         boolean onPaste(ClipData clip);
+        void onMemoryRecall();
     }
 }
diff --git a/src/com/android/calculator2/CalculatorPadViewPager.java b/src/com/android/calculator2/CalculatorPadViewPager.java
index 560260b..9197342 100644
--- a/src/com/android/calculator2/CalculatorPadViewPager.java
+++ b/src/com/android/calculator2/CalculatorPadViewPager.java
@@ -21,6 +21,7 @@
 import android.support.v4.view.PagerAdapter;
 import android.support.v4.view.ViewPager;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.GestureDetector;
 import android.view.MotionEvent;
 import android.view.View;
@@ -186,48 +187,66 @@
 
     @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
-        // Always intercept touch events when a11y focused since otherwise they will be
-        // incorrectly offset by a11y before being dispatched to children.
-        boolean shouldIntercept = isAccessibilityFocused() || super.onInterceptTouchEvent(ev);
+        try {
+            // Always intercept touch events when a11y focused since otherwise they will be
+            // incorrectly offset by a11y before being dispatched to children.
+            if (isAccessibilityFocused() || super.onInterceptTouchEvent(ev)) {
+                return true;
+            }
 
-        // Only allow the current item to receive touch events.
-        if (!shouldIntercept && ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
-            final int x = (int) ev.getX() + getScrollX();
-            final int y = (int) ev.getY() + getScrollY();
+            // Only allow the current item to receive touch events.
+            final int action = ev.getActionMasked();
+            if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) {
+                // If a child is a11y focused then we must always intercept the touch event
+                // since it will be incorrectly offset by a11y.
+                final int childCount = getChildCount();
+                for (int childIndex = childCount - 1; childIndex >= 0; --childIndex) {
+                    if (getChildAt(childIndex).isAccessibilityFocused()) {
+                        mClickedItemIndex = childIndex;
+                        return true;
+                    }
+                }
 
-            // Reset the previously clicked item index.
-            mClickedItemIndex = -1;
+                if (action == MotionEvent.ACTION_DOWN) {
+                    mClickedItemIndex = -1;
+                }
 
-            final int childCount = getChildCount();
-            for (int i = childCount - 1; i >= 0; --i) {
-                final int childIndex = getChildDrawingOrder(childCount, i);
-                final View child = getChildAt(childIndex);
-                if (child.isAccessibilityFocused()) {
-                    // If a child is a11y focused then we must always intercept the touch event
-                    // since it will be incorrectly offset by a11y.
-                    shouldIntercept = true;
-                    mClickedItemIndex = childIndex;
-                    break;
-                } else if (mClickedItemIndex == -1
-                        && child.getVisibility() == VISIBLE
-                        && x >= child.getLeft() && x < child.getRight()
-                        && y >= child.getTop() && y < child.getBottom()) {
-                    shouldIntercept = childIndex != getCurrentItem();
-                    mClickedItemIndex = childIndex;
-                    // continue; since another child may be a11y focused.
+                // Otherwise if touch is on a non-current item then intercept.
+                final int actionIndex = ev.getActionIndex();
+                final float x = ev.getX(actionIndex) + getScrollX();
+                final float y = ev.getY(actionIndex) + getScrollY();
+                for (int i = childCount - 1; i >= 0; --i) {
+                    final int childIndex = getChildDrawingOrder(childCount, i);
+                    final View child = getChildAt(childIndex);
+                    if (child.getVisibility() == VISIBLE
+                            && x >= child.getLeft() && x < child.getRight()
+                            && y >= child.getTop() && y < child.getBottom()) {
+                        if (action == MotionEvent.ACTION_DOWN) {
+                            mClickedItemIndex = childIndex;
+                        }
+                        return childIndex != getCurrentItem();
+                    }
                 }
             }
-        }
 
-        return shouldIntercept;
+            return false;
+        } catch (IllegalArgumentException e) {
+            Log.e("Calculator", "Error intercepting touch event", e);
+            return false;
+        }
     }
 
     @Override
     public boolean onTouchEvent(MotionEvent ev) {
-        // Allow both the gesture detector and super to handle the touch event so they both see
-        // the full sequence of events. This should be safe since the gesture detector only
-        // handle clicks and super only handles swipes.
-        mGestureDetector.onTouchEvent(ev);
-        return super.onTouchEvent(ev);
+        try {
+            // Allow both the gesture detector and super to handle the touch event so they both see
+            // the full sequence of events. This should be safe since the gesture detector only
+            // handle clicks and super only handles swipes.
+            mGestureDetector.onTouchEvent(ev);
+            return super.onTouchEvent(ev);
+        } catch (IllegalArgumentException e) {
+            Log.e("Calculator", "Error processing touch event", e);
+            return false;
+        }
     }
 }
diff --git a/src/com/android/calculator2/CalculatorResult.java b/src/com/android/calculator2/CalculatorResult.java
index 234f602..d2ba9a8 100644
--- a/src/com/android/calculator2/CalculatorResult.java
+++ b/src/com/android/calculator2/CalculatorResult.java
@@ -23,6 +23,7 @@
 import android.content.Context;
 import android.graphics.Rect;
 import android.os.Build;
+import android.support.annotation.IntDef;
 import android.support.v4.content.ContextCompat;
 import android.support.v4.os.BuildCompat;
 import android.text.Layout;
@@ -42,23 +43,28 @@
 import android.view.MenuItem;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.ViewConfiguration;
 import android.widget.OverScroller;
 import android.widget.Toast;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 // 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.
     private boolean mValid = false;
-                            // The result holds something valid; either a a number or an error
-                            // message.
+                            // The result holds a valid number (not an error message).
     // A suffix of "Pos" denotes a pixel offset.  Zero represents a scroll position
     // in which the decimal point is just barely visible on the right of the display.
     private int mCurrentPos;// Position of right of display relative to decimal point, in pixels.
@@ -94,10 +100,11 @@
                             // append an exponent insteadd of replacing trailing digits.
     private final Object mWidthLock = new Object();
                             // Protects the next five fields.  These fields are only
-                            // Updated by the UI thread, and read accesses by the UI thread
+                            // updated by the UI thread, and read accesses by the UI thread
                             // sometimes do not acquire the lock.
-    private int mWidthConstraint = -1;
+    private int mWidthConstraint = 0;
                             // Our total width in pixels minus space for ellipsis.
+                            // 0 ==> uninitialized.
     private float mCharWidth = 1;
                             // Maximum character width. For now we pretend that all characters
                             // have this width.
@@ -111,8 +118,16 @@
     private float mNoEllipsisCredit;
                             // Fraction of digit width saved by both replacing ellipsis with digit
                             // and avoiding scientific notation.
-    private static final int MAX_WIDTH = 100;
-                            // Maximum number of digits displayed.
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({SHOULD_REQUIRE, SHOULD_EVALUATE, SHOULD_NOT_EVALUATE})
+    public @interface EvaluationRequest {}
+    public static final int SHOULD_REQUIRE = 2;
+    public static final int SHOULD_EVALUATE = 1;
+    public static final int SHOULD_NOT_EVALUATE = 0;
+    @EvaluationRequest private int mEvaluationRequest = SHOULD_REQUIRE;
+                            // Should we evaluate when layout completes, and how?
+    private Evaluator.EvaluationListener mEvaluationListener = this;
+                            // Listener to use if/when evaluation is requested.
     public static final int MAX_LEADING_ZEROES = 6;
                             // Maximum number of leading zeroes after decimal point before we
                             // switch to scientific notation with negative exponent.
@@ -138,6 +153,9 @@
     private ActionMode.Callback mCopyActionModeCallback;
     private ContextMenu mContextMenu;
 
+    // The user requested that the result currently being evaluated should be stored to "memory".
+    private boolean mStoreToMemoryRequested = false;
+
     public CalculatorResult(Context context, AttributeSet attrs) {
         super(context, attrs);
         mScroller = new OverScroller(context);
@@ -151,8 +169,8 @@
                     return true;
                 }
                 @Override
-                public boolean onFling(MotionEvent e1, MotionEvent e2,
-                                       float velocityX, float velocityY) {
+                public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+                        float velocityY) {
                     if (!mScroller.isFinished()) {
                         mCurrentPos = mScroller.getFinalX();
                     }
@@ -167,8 +185,8 @@
                     return true;
                 }
                 @Override
-                public boolean onScroll(MotionEvent e1, MotionEvent e2,
-                                        float distanceX, float distanceY) {
+                public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
+                        float distanceY) {
                     int distance = (int)distanceX;
                     if (!mScroller.isFinished()) {
                         mCurrentPos = mScroller.getFinalX();
@@ -195,9 +213,33 @@
                     }
                 }
             });
+
+        final int slop = ViewConfiguration.get(context).getScaledTouchSlop();
         setOnTouchListener(new View.OnTouchListener() {
+
+            // Used to determine whether a touch event should be intercepted.
+            private float mInitialDownX;
+            private float mInitialDownY;
+
             @Override
             public boolean onTouch(View v, MotionEvent event) {
+                final int action = event.getActionMasked();
+
+                final float x = event.getX();
+                final float y = event.getY();
+                switch (action) {
+                    case MotionEvent.ACTION_DOWN:
+                        mInitialDownX = x;
+                        mInitialDownY = y;
+                        break;
+                    case MotionEvent.ACTION_MOVE:
+                        final float deltaX = Math.abs(x - mInitialDownX);
+                        final float deltaY = Math.abs(y - mInitialDownY);
+                        if (deltaX > slop && deltaX > deltaY) {
+                            // Prevent the DragLayout from intercepting horizontal scrolls.
+                            getParent().requestDisallowInterceptTouchEvent(true);
+                        }
+                }
                 return mGestureDetector.onTouchEvent(event);
             }
         });
@@ -209,10 +251,14 @@
         }
 
         setCursorVisible(false);
+        setLongClickable(false);
+        setContentDescription(context.getString(R.string.desc_result));
     }
 
-    void setEvaluator(Evaluator evaluator) {
+    void setEvaluator(Evaluator evaluator, long index) {
         mEvaluator = evaluator;
+        mIndex = index;
+        requestLayout();
     }
 
     // Compute maximum digit width the hard way.
@@ -233,6 +279,7 @@
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         if (!isLaidOut()) {
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
             // Set a minimum height so scaled error messages won't affect our layout.
             setMinimumHeight(getLineHeight() + getCompoundPaddingBottom()
                     + getCompoundPaddingTop());
@@ -298,12 +345,34 @@
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
     }
 
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+
+        if (mEvaluator != null && mEvaluationRequest != SHOULD_NOT_EVALUATE) {
+            final CalculatorExpr expr = mEvaluator.getExpr(mIndex);
+            if (expr != null && expr.hasInterestingOps()) {
+                if (mEvaluationRequest == SHOULD_REQUIRE) {
+                    mEvaluator.requireResult(mIndex, mEvaluationListener, this);
+                } else {
+                    mEvaluator.evaluateAndNotify(mIndex, mEvaluationListener, this);
+                }
+            }
+        }
+    }
+
     /**
-     * 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.
+     * Specify whether we should evaluate result on layout.
+     * @param should one of SHOULD_REQUIRE, SHOULD_EVALUATE, SHOULD_NOT_EVALUATE
      */
+    public void setShouldEvaluateResult(@EvaluationRequest int request,
+            Evaluator.EvaluationListener listener) {
+        mEvaluationListener = listener;
+        mEvaluationRequest = request;
+    }
+
+    // From Evaluator.CharMetricsInfo.
+    @Override
     public float separatorChars(String s, int len) {
         int start = 0;
         while (start < len && !Character.isDigit(s.charAt(start))) {
@@ -320,20 +389,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;
@@ -353,6 +418,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.
@@ -361,12 +428,47 @@
      * @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);
+
+        if (mStoreToMemoryRequested) {
+            mEvaluator.copyToMemory(index);
+            mStoreToMemoryRequested = false;
+        }
         redisplay();
     }
 
     /**
+     * Store the result for this index if it is available.
+     * If it is unavailable, set mStoreToMemoryRequested to indicate that we should store
+     * when evaluation is complete.
+     */
+    public void onMemoryStore() {
+        if (mEvaluator.hasResult(mIndex)) {
+            mEvaluator.copyToMemory(mIndex);
+        } else {
+            mStoreToMemoryRequested = true;
+            mEvaluator.requireResult(mIndex, this /* listener */, this /* CharMetricsInfo */);
+        }
+    }
+
+    /**
+     * Add the result to the value currently in memory.
+     */
+    public void onMemoryAdd() {
+        mEvaluator.addToMemory(mIndex);
+    }
+
+    /**
+     * Subtract the result from the value currently in memory.
+     */
+    public void onMemorySubtract() {
+        mEvaluator.subtractFromMemory(mIndex);
+    }
+
+    /**
      * Set up scroll bounds (mMinPos, mMaxPos, etc.) and determine whether the result is
      * scrollable, based on the supplied information about the result.
      * This is unfortunately complicated because we need to predict whether trailing digits
@@ -490,8 +592,11 @@
      * Display error message indicated by resourceId.
      * UI thread only.
      */
-    void displayError(int resourceId) {
-        mValid = true;
+    @Override
+    public void onError(long index, int resourceId) {
+        mStoreToMemoryRequested = false;
+        mValid = false;
+        setLongClickable(false);
         mScrollable = false;
         final String msg = getContext().getString(resourceId);
         final float measuredWidth = Layout.getDesiredWidth(msg, getPaint());
@@ -639,6 +744,10 @@
                         ++exponent;
                     }
                 }
+                if (dropDigits >= result.length() - 1) {
+                    // Display too small to show meaningful result.
+                    return KeyMaps.ELLIPSIS + "E" + KeyMaps.ELLIPSIS;
+                }
                 result = result.substring(0, result.length() - dropDigits);
                 if (lastDisplayedOffset != null) {
                     lastDisplayedOffset[0] -= dropDigits;
@@ -709,8 +818,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);
    }
@@ -731,7 +840,7 @@
      * UI thread only.
      */
     public boolean fullTextIsExact() {
-        return !mScrollable || (mMaxCharOffset == getCharOffset(mCurrentPos)
+        return !mScrollable || (getCharOffset(mMaxPos) == getCharOffset(mCurrentPos)
                 && mMaxCharOffset != MAX_RIGHT_SCROLL);
     }
 
@@ -749,9 +858,14 @@
             return getFullText(false /* withSeparators */);
         }
         // 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 formattedResult = formatResult(rawResult, nonNegLsdOffset, MAX_COPY_SIZE,
+        int fractionLsdOffset = Math.max(0, mLsdOffset);
+        String rawResult = mEvaluator.getResult(mIndex).toStringTruncated(fractionLsdOffset);
+        if (mLsdOffset <= -1) {
+            // Result has trailing decimal point. Remove it.
+            rawResult = rawResult.substring(0, rawResult.length() - 1);
+            fractionLsdOffset = -1;
+        }
+        final String formattedResult = formatResult(rawResult, fractionLsdOffset, MAX_COPY_SIZE,
                 false, rawResult.charAt(0) == '-', null, true /* forcePrecision */,
                 false /* forceSciNotation */, false /* insertCommas */);
         return KeyMaps.translateResult(formattedResult);
@@ -759,20 +873,14 @@
 
     /**
      * 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.
+     * Returns zero if measurement hasn't completed.
      */
-    int getMaxChars() {
+    @Override
+    public int getMaxChars() {
         int result;
         synchronized(mWidthLock) {
-            result = (int) Math.floor(mWidthConstraint / mCharWidth);
-            // We can apparently finish evaluating before onMeasure in CalculatorText has been
-            // called, in which case we get 0 or -1 as the width constraint.
-        }
-        if (result <= 0) {
-            // Return something conservatively big, to force sufficient evaluation.
-            return MAX_WIDTH;
-        } else {
-            return result;
+            return (int) Math.floor(mWidthConstraint / mCharWidth);
         }
     }
 
@@ -795,15 +903,34 @@
         mValid = false;
         mScrollable = false;
         setText("");
+        setLongClickable(false);
+    }
+
+    @Override
+    public void onCancelled(long index) {
+        clear();
+        mStoreToMemoryRequested = false;
     }
 
     /**
      * Refresh display.
-     * Only called in UI thread.
+     * Only called in UI thread. Index argument is currently ignored.
      */
-    void redisplay() {
-        int currentCharOffset = getCharOffset(mCurrentPos);
+    @Override
+    public void onReevaluate(long index) {
+        redisplay();
+    }
+
+    public void redisplay() {
         int maxChars = getMaxChars();
+        if (maxChars < 4) {
+            // Display currently too small to display a reasonable result. Punt to avoid crash.
+            return;
+        }
+        if (mScroller.isFinished() && length() > 0) {
+            setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
+        }
+        int currentCharOffset = getCharOffset(mCurrentPos);
         int lastDisplayedOffset[] = new int[1];
         String result = getFormattedResult(currentCharOffset, maxChars, lastDisplayedOffset,
                 mAppendExponent /* forcePrecision; preserve entire result */,
@@ -823,25 +950,49 @@
         }
         mLastDisplayedOffset = lastDisplayedOffset[0];
         mValid = true;
+        setLongClickable(true);
+    }
+
+    @Override
+    protected void onTextChanged(java.lang.CharSequence text, int start, int lengthBefore,
+            int lengthAfter) {
+        super.onTextChanged(text, start, lengthBefore, lengthAfter);
+
+        if (!mScrollable || mScroller.isFinished()) {
+            if (lengthBefore == 0 && lengthAfter > 0) {
+                setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
+                setContentDescription(null);
+            } else if (lengthBefore > 0 && lengthAfter == 0) {
+                setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_NONE);
+                setContentDescription(getContext().getString(R.string.desc_result));
+            }
+        }
     }
 
     @Override
     public void computeScroll() {
-        if (!mScrollable) return;
+        if (!mScrollable) {
+            return;
+        }
+
         if (mScroller.computeScrollOffset()) {
             mCurrentPos = mScroller.getCurrX();
             if (getCharOffset(mCurrentPos) != getCharOffset(mLastPos)) {
                 mLastPos = mCurrentPos;
                 redisplay();
             }
-            if (!mScroller.isFinished()) {
+        }
+
+        if (!mScroller.isFinished()) {
                 postInvalidateOnAnimation();
-            }
+                setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_NONE);
+        } else if (length() > 0){
+            setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
         }
     }
 
     /**
-     * Use ActionMode for copy support on M and higher.
+     * Use ActionMode for copy/memory support on M and higher.
      */
     @TargetApi(Build.VERSION_CODES.M)
     private void setupActionMode() {
@@ -850,7 +1001,7 @@
             @Override
             public boolean onCreateActionMode(ActionMode mode, Menu menu) {
                 final MenuInflater inflater = mode.getMenuInflater();
-                return createCopyMenu(inflater, menu);
+                return createContextMenu(inflater, menu);
             }
 
             @Override
@@ -916,7 +1067,7 @@
     }
 
     /**
-     * Use ContextMenu for copy support on L and lower.
+     * Use ContextMenu for copy/memory support on L and lower.
      */
     private void setupContextMenu() {
         setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
@@ -924,9 +1075,9 @@
             public void onCreateContextMenu(ContextMenu contextMenu, View view,
                     ContextMenu.ContextMenuInfo contextMenuInfo) {
                 final MenuInflater inflater = new MenuInflater(getContext());
-                createCopyMenu(inflater, contextMenu);
+                createContextMenu(inflater, contextMenu);
                 mContextMenu = contextMenu;
-                for(int i = 0; i < contextMenu.size(); i ++) {
+                for (int i = 0; i < contextMenu.size(); i ++) {
                     contextMenu.getItem(i).setOnMenuItemClickListener(CalculatorResult.this);
                 }
             }
@@ -942,8 +1093,13 @@
         });
     }
 
-    private boolean createCopyMenu(MenuInflater inflater, Menu menu) {
-        inflater.inflate(R.menu.copy, menu);
+    private boolean createContextMenu(MenuInflater inflater, Menu menu) {
+        inflater.inflate(R.menu.menu_result, menu);
+        final boolean displayMemory = mEvaluator.getMemoryIndex() != 0;
+        final MenuItem memoryAddItem = menu.findItem(R.id.memory_add);
+        final MenuItem memorySubtractItem = menu.findItem(R.id.memory_subtract);
+        memoryAddItem.setEnabled(displayMemory);
+        memorySubtractItem.setEnabled(displayMemory);
         highlightResult();
         return true;
     }
@@ -983,7 +1139,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);
@@ -993,8 +1149,17 @@
     @Override
     public boolean onMenuItemClick(MenuItem item) {
         switch (item.getItemId()) {
+            case R.id.memory_add:
+                onMemoryAdd();
+                return true;
+            case R.id.memory_subtract:
+                onMemorySubtract();
+                return true;
+            case R.id.memory_store:
+                onMemoryStore();
+                return true;
             case R.id.menu_copy:
-                if (mEvaluator.reevaluationInProgress()) {
+                if (mEvaluator.evaluationInProgress(mIndex)) {
                     // Refuse to copy placeholder characters.
                     return false;
                 } else {
@@ -1006,4 +1171,10 @@
                 return false;
         }
     }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        stopActionModeOrContextMenu();
+        super.onDetachedFromWindow();
+    }
 }
diff --git a/src/com/android/calculator2/CalculatorScrollView.java b/src/com/android/calculator2/CalculatorScrollView.java
index bcf5650..018ad10 100644
--- a/src/com/android/calculator2/CalculatorScrollView.java
+++ b/src/com/android/calculator2/CalculatorScrollView.java
@@ -22,6 +22,10 @@
 import android.view.ViewGroup;
 import android.widget.HorizontalScrollView;
 
+import static android.view.View.MeasureSpec.UNSPECIFIED;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+
 public class CalculatorScrollView extends HorizontalScrollView {
 
     public CalculatorScrollView(Context context) {
@@ -36,17 +40,26 @@
         super(context, attrs, defStyleAttr);
     }
 
+    private static int getChildMeasureSpecCompat(int spec, int padding, int childDimension) {
+        if (MeasureSpec.getMode(spec) == UNSPECIFIED
+                && (childDimension == MATCH_PARENT || childDimension == WRAP_CONTENT)) {
+            final int size = Math.max(0, MeasureSpec.getSize(spec) - padding);
+            return MeasureSpec.makeMeasureSpec(size, UNSPECIFIED);
+        }
+        return ViewGroup.getChildMeasureSpec(spec, padding, childDimension);
+    }
+
     @Override
     protected void measureChild(View child, int parentWidthMeasureSpec,
             int parentHeightMeasureSpec) {
         // Allow child to be as wide as they want.
         parentWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
-                MeasureSpec.getSize(parentWidthMeasureSpec), MeasureSpec.UNSPECIFIED);
+                MeasureSpec.getSize(parentWidthMeasureSpec), UNSPECIFIED);
 
         final ViewGroup.LayoutParams lp = child.getLayoutParams();
-        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
+        final int childWidthMeasureSpec = getChildMeasureSpecCompat(parentWidthMeasureSpec,
                 0 /* padding */, lp.width);
-        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
+        final int childHeightMeasureSpec = getChildMeasureSpecCompat(parentHeightMeasureSpec,
                 getPaddingTop() + getPaddingBottom(), lp.height);
 
         child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
@@ -57,12 +70,12 @@
             int parentHeightMeasureSpec, int heightUsed) {
         // Allow child to be as wide as they want.
         parentWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
-                MeasureSpec.getSize(parentWidthMeasureSpec), MeasureSpec.UNSPECIFIED);
+                MeasureSpec.getSize(parentWidthMeasureSpec), UNSPECIFIED);
 
         final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
-        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
+        final int childWidthMeasureSpec = getChildMeasureSpecCompat(parentWidthMeasureSpec,
                 lp.leftMargin + lp.rightMargin, lp.width);
-        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
+        final int childHeightMeasureSpec = getChildMeasureSpecCompat(parentHeightMeasureSpec,
                 getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin, lp.height);
 
         child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
diff --git a/src/com/android/calculator2/DragController.java b/src/com/android/calculator2/DragController.java
new file mode 100644
index 0000000..1716cc9
--- /dev/null
+++ b/src/com/android/calculator2/DragController.java
@@ -0,0 +1,482 @@
+/*
+ * 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.
+ */
+
+package com.android.calculator2;
+
+import android.animation.ArgbEvaluator;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * Contains the logic for animating the recyclerview elements on drag.
+ */
+public final class DragController {
+
+    private static final String TAG = "DragController";
+
+    private static final ArgbEvaluator mColorEvaluator = new ArgbEvaluator();
+
+    // References to views from the Calculator Display.
+    private CalculatorFormula mDisplayFormula;
+    private CalculatorResult mDisplayResult;
+    private View mToolbar;
+
+    private int mFormulaTranslationY;
+    private int mFormulaTranslationX;
+    private float mFormulaScale;
+    private float mResultScale;
+
+    private float mResultTranslationY;
+    private int mResultTranslationX;
+
+    private int mDisplayHeight;
+
+    private int mFormulaStartColor;
+    private int mFormulaEndColor;
+
+    private int mResultStartColor;
+    private int mResultEndColor;
+
+    // The padding at the bottom of the RecyclerView itself.
+    private int mBottomPaddingHeight;
+
+    private boolean mAnimationInitialized;
+
+    private boolean mOneLine;
+    private boolean mIsDisplayEmpty;
+
+    private AnimationController mAnimationController;
+
+    private Evaluator mEvaluator;
+
+    public void setEvaluator(Evaluator evaluator) {
+        mEvaluator = evaluator;
+    }
+
+    public void initializeController(boolean isResult, boolean oneLine, boolean isDisplayEmpty) {
+        mOneLine = oneLine;
+        mIsDisplayEmpty = isDisplayEmpty;
+        if (mIsDisplayEmpty) {
+            // Empty display
+            mAnimationController = new EmptyAnimationController();
+        } else if (isResult) {
+            // Result
+            mAnimationController = new ResultAnimationController();
+        } else {
+            // There is something in the formula field. There may or may not be
+            // a quick result.
+            mAnimationController = new AnimationController();
+        }
+    }
+
+    public void setDisplayFormula(CalculatorFormula formula) {
+        mDisplayFormula = formula;
+    }
+
+    public void setDisplayResult(CalculatorResult result) {
+        mDisplayResult = result;
+    }
+
+    public void setToolbar(View toolbar) {
+        mToolbar = toolbar;
+    }
+
+    public void animateViews(float yFraction, RecyclerView recyclerView) {
+        if (mDisplayFormula == null
+                || mDisplayResult == null
+                || mToolbar == null
+                || mEvaluator == null) {
+            // Bail if we aren't yet initialized.
+            return;
+        }
+
+        final HistoryAdapter.ViewHolder vh =
+                (HistoryAdapter.ViewHolder) recyclerView.findViewHolderForAdapterPosition(0);
+        if (yFraction > 0 && vh != null) {
+            recyclerView.setVisibility(View.VISIBLE);
+        }
+        if (vh != null && !mIsDisplayEmpty
+                && vh.getItemViewType() == HistoryAdapter.HISTORY_VIEW_TYPE) {
+            final AlignedTextView formula = vh.getFormula();
+            final CalculatorResult result = vh.getResult();
+            final TextView date = vh.getDate();
+            final View divider = vh.getDivider();
+
+            if (!mAnimationInitialized) {
+                mBottomPaddingHeight = recyclerView.getPaddingBottom();
+
+                mAnimationController.initializeScales(formula, result);
+
+                mAnimationController.initializeColorAnimators(formula, result);
+
+                mAnimationController.initializeFormulaTranslationX(formula);
+
+                mAnimationController.initializeFormulaTranslationY(formula, result);
+
+                mAnimationController.initializeResultTranslationX(result);
+
+                mAnimationController.initializeResultTranslationY(result);
+
+                mAnimationInitialized = true;
+            }
+
+            result.setScaleX(mAnimationController.getResultScale(yFraction));
+            result.setScaleY(mAnimationController.getResultScale(yFraction));
+
+            formula.setScaleX(mAnimationController.getFormulaScale(yFraction));
+            formula.setScaleY(mAnimationController.getFormulaScale(yFraction));
+
+            formula.setPivotX(formula.getWidth() - formula.getPaddingEnd());
+            formula.setPivotY(formula.getHeight() - formula.getPaddingBottom());
+
+            result.setPivotX(result.getWidth() - result.getPaddingEnd());
+            result.setPivotY(result.getHeight() - result.getPaddingBottom());
+
+            formula.setTranslationX(mAnimationController.getFormulaTranslationX(yFraction));
+            formula.setTranslationY(mAnimationController.getFormulaTranslationY(yFraction));
+
+            result.setTranslationX(mAnimationController.getResultTranslationX(yFraction));
+            result.setTranslationY(mAnimationController.getResultTranslationY(yFraction));
+
+            formula.setTextColor((int) mColorEvaluator.evaluate(yFraction, mFormulaStartColor,
+                    mFormulaEndColor));
+
+            result.setTextColor((int) mColorEvaluator.evaluate(yFraction, mResultStartColor,
+                    mResultEndColor));
+
+            date.setTranslationY(mAnimationController.getDateTranslationY(yFraction));
+            divider.setTranslationY(mAnimationController.getDateTranslationY(yFraction));
+        } else if (mIsDisplayEmpty) {
+            // There is no current expression but we still need to collect information
+            // to translate the other viewholders.
+            if (!mAnimationInitialized) {
+                mAnimationController.initializeDisplayHeight();
+                mAnimationInitialized = true;
+            }
+        }
+
+        // Move up all ViewHolders above the current expression; if there is no current expression,
+        // we're translating all the viewholders.
+        for (int i = recyclerView.getChildCount() - 1;
+             i >= mAnimationController.getFirstTranslatedViewHolderIndex();
+             --i) {
+            final RecyclerView.ViewHolder vh2 =
+                    recyclerView.getChildViewHolder(recyclerView.getChildAt(i));
+            if (vh2 != null) {
+                final View view = vh2.itemView;
+                if (view != null) {
+                    view.setTranslationY(
+                        mAnimationController.getHistoryElementTranslationY(yFraction));
+                }
+            }
+        }
+    }
+
+    /**
+     * Reset all initialized values.
+     */
+    public void initializeAnimation(boolean isResult, boolean oneLine, boolean isDisplayEmpty) {
+        mAnimationInitialized = false;
+        initializeController(isResult, oneLine, isDisplayEmpty);
+    }
+
+    public interface AnimateTextInterface {
+
+        void initializeDisplayHeight();
+
+        void initializeColorAnimators(AlignedTextView formula, CalculatorResult result);
+
+        void initializeScales(AlignedTextView formula, CalculatorResult result);
+
+        void initializeFormulaTranslationX(AlignedTextView formula);
+
+        void initializeFormulaTranslationY(AlignedTextView formula, CalculatorResult result);
+
+        void initializeResultTranslationX(CalculatorResult result);
+
+        void initializeResultTranslationY(CalculatorResult result);
+
+        float getResultTranslationX(float yFraction);
+
+        float getResultTranslationY(float yFraction);
+
+        float getResultScale(float yFraction);
+
+        float getFormulaScale(float yFraction);
+
+        float getFormulaTranslationX(float yFraction);
+
+        float getFormulaTranslationY(float yFraction);
+
+        float getDateTranslationY(float yFraction);
+
+        float getHistoryElementTranslationY(float yFraction);
+
+        // Return the lowest index of the first Viewholder to be translated upwards.
+        // If there is no current expression, we translate all the viewholders; otherwise,
+        // we start at index 1.
+        int getFirstTranslatedViewHolderIndex();
+    }
+
+    // The default AnimationController when Display is in INPUT state and DisplayFormula is not
+    // empty. There may or may not be a quick result.
+    public class AnimationController implements DragController.AnimateTextInterface {
+
+        public void initializeDisplayHeight() {
+            // no-op
+        }
+
+        public void initializeColorAnimators(AlignedTextView formula, CalculatorResult result) {
+            mFormulaStartColor = mDisplayFormula.getCurrentTextColor();
+            mFormulaEndColor = formula.getCurrentTextColor();
+
+            mResultStartColor = mDisplayResult.getCurrentTextColor();
+            mResultEndColor = result.getCurrentTextColor();
+        }
+
+        public void initializeScales(AlignedTextView formula, CalculatorResult result) {
+            // Calculate the scale for the text
+            mFormulaScale = mDisplayFormula.getTextSize() / formula.getTextSize();
+        }
+
+        public void initializeFormulaTranslationY(AlignedTextView formula,
+                CalculatorResult result) {
+            if (mOneLine) {
+                // Disregard result since we set it to GONE in the one-line case.
+                mFormulaTranslationY =
+                        mDisplayFormula.getPaddingBottom() - formula.getPaddingBottom()
+                        - mBottomPaddingHeight;
+            } else {
+                // Baseline of formula moves by the difference in formula bottom padding and the
+                // difference in result height.
+                mFormulaTranslationY =
+                        mDisplayFormula.getPaddingBottom() - formula.getPaddingBottom()
+                                + mDisplayResult.getHeight() - result.getHeight()
+                                - mBottomPaddingHeight;
+            }
+        }
+
+        public void initializeFormulaTranslationX(AlignedTextView formula) {
+            // Right border of formula moves by the difference in formula end padding.
+            mFormulaTranslationX = mDisplayFormula.getPaddingEnd() - formula.getPaddingEnd();
+        }
+
+        public void initializeResultTranslationY(CalculatorResult result) {
+            // Baseline of result moves by the difference in result bottom padding.
+            mResultTranslationY = mDisplayResult.getPaddingBottom() - result.getPaddingBottom()
+            - mBottomPaddingHeight;
+        }
+
+        public void initializeResultTranslationX(CalculatorResult result) {
+            mResultTranslationX = mDisplayResult.getPaddingEnd() - result.getPaddingEnd();
+        }
+
+        public float getResultTranslationX(float yFraction) {
+            return mResultTranslationX * (yFraction - 1f);
+        }
+
+        public float getResultTranslationY(float yFraction) {
+            return mResultTranslationY * (yFraction - 1f);
+        }
+
+        public float getResultScale(float yFraction) {
+            return 1f;
+        }
+
+        public float getFormulaScale(float yFraction) {
+            return mFormulaScale + (1f - mFormulaScale) * yFraction;
+        }
+
+        public float getFormulaTranslationX(float yFraction) {
+            return mFormulaTranslationX * (yFraction - 1f);
+        }
+
+        public float getFormulaTranslationY(float yFraction) {
+            // Scale linearly between -FormulaTranslationY and 0.
+            return mFormulaTranslationY * (yFraction - 1f);
+        }
+
+        public float getDateTranslationY(float yFraction) {
+            // We also want the date to start out above the visible screen with
+            // this distance decreasing as it's pulled down.
+            // Account for the scaled formula height.
+            return -mToolbar.getHeight() * (1f - yFraction)
+                    + getFormulaTranslationY(yFraction)
+                    - mDisplayFormula.getHeight() /getFormulaScale(yFraction) * (1f - yFraction);
+        }
+
+        public float getHistoryElementTranslationY(float yFraction) {
+            return getDateTranslationY(yFraction);
+        }
+
+        public int getFirstTranslatedViewHolderIndex() {
+            return 1;
+        }
+    }
+
+    // The default AnimationController when Display is in RESULT state.
+    public class ResultAnimationController extends AnimationController
+            implements DragController.AnimateTextInterface {
+        @Override
+        public void initializeScales(AlignedTextView formula, CalculatorResult result) {
+            final float textSize = mDisplayResult.getTextSize() * mDisplayResult.getScaleX();
+            mResultScale = textSize / result.getTextSize();
+            mFormulaScale = 1f;
+        }
+
+        @Override
+        public void initializeFormulaTranslationY(AlignedTextView formula,
+                CalculatorResult result) {
+            // Baseline of formula moves by the difference in formula bottom padding and the
+            // difference in the result height.
+            mFormulaTranslationY = mDisplayFormula.getPaddingBottom() - formula.getPaddingBottom()
+                            + mDisplayResult.getHeight() - result.getHeight()
+                            - mBottomPaddingHeight;
+        }
+
+        @Override
+        public void initializeFormulaTranslationX(AlignedTextView formula) {
+            // Right border of formula moves by the difference in formula end padding.
+            mFormulaTranslationX = mDisplayFormula.getPaddingEnd() - formula.getPaddingEnd();
+        }
+
+        @Override
+        public void initializeResultTranslationY(CalculatorResult result) {
+            // Baseline of result moves by the difference in result bottom padding.
+            mResultTranslationY =  mDisplayResult.getPaddingBottom() - result.getPaddingBottom()
+                    - mDisplayResult.getTranslationY()
+                    - mBottomPaddingHeight;
+        }
+
+        @Override
+        public void initializeResultTranslationX(CalculatorResult result) {
+            mResultTranslationX = mDisplayResult.getPaddingEnd() - result.getPaddingEnd();
+        }
+
+        @Override
+        public float getResultTranslationX(float yFraction) {
+            return (mResultTranslationX * yFraction) - mResultTranslationX;
+        }
+
+        @Override
+        public float getResultTranslationY(float yFraction) {
+            return (mResultTranslationY * yFraction) - mResultTranslationY;
+        }
+
+        @Override
+        public float getFormulaTranslationX(float yFraction) {
+            return (mFormulaTranslationX * yFraction) -
+                    mFormulaTranslationX;
+        }
+
+        @Override
+        public float getFormulaTranslationY(float yFraction) {
+            return getDateTranslationY(yFraction);
+        }
+
+        @Override
+        public float getResultScale(float yFraction) {
+            return mResultScale - (mResultScale * yFraction) + yFraction;
+        }
+
+        @Override
+        public float getFormulaScale(float yFraction) {
+            return 1f;
+        }
+
+        @Override
+        public float getDateTranslationY(float yFraction) {
+            // We also want the date to start out above the visible screen with
+            // this distance decreasing as it's pulled down.
+            return -mToolbar.getHeight() * (1f - yFraction)
+                    + (mResultTranslationY * yFraction) - mResultTranslationY
+                    - mDisplayFormula.getPaddingTop() +
+                    (mDisplayFormula.getPaddingTop() * yFraction);
+        }
+
+        @Override
+        public int getFirstTranslatedViewHolderIndex() {
+            return 1;
+        }
+    }
+
+    // The default AnimationController when Display is completely empty.
+    public class EmptyAnimationController extends AnimationController
+            implements DragController.AnimateTextInterface {
+        @Override
+        public void initializeDisplayHeight() {
+            mDisplayHeight = mToolbar.getHeight() + mDisplayResult.getHeight()
+                    + mDisplayFormula.getHeight();
+        }
+
+        @Override
+        public void initializeScales(AlignedTextView formula, CalculatorResult result) {
+            // no-op
+        }
+
+        @Override
+        public void initializeFormulaTranslationY(AlignedTextView formula,
+                CalculatorResult result) {
+            // no-op
+        }
+
+        @Override
+        public void initializeFormulaTranslationX(AlignedTextView formula) {
+            // no-op
+        }
+
+        @Override
+        public void initializeResultTranslationY(CalculatorResult result) {
+            // no-op
+        }
+
+        @Override
+        public void initializeResultTranslationX(CalculatorResult result) {
+            // no-op
+        }
+
+        @Override
+        public float getResultTranslationX(float yFraction) {
+            return 0f;
+        }
+
+        @Override
+        public float getResultTranslationY(float yFraction) {
+            return 0f;
+        }
+
+        @Override
+        public float getFormulaScale(float yFraction) {
+            return 1f;
+        }
+
+        @Override
+        public float getDateTranslationY(float yFraction) {
+            return 0f;
+        }
+
+        @Override
+        public float getHistoryElementTranslationY(float yFraction) {
+            return -mDisplayHeight * (1f - yFraction) - mBottomPaddingHeight;
+        }
+
+        @Override
+        public int getFirstTranslatedViewHolderIndex() {
+            return 0;
+        }
+    }
+}
diff --git a/src/com/android/calculator2/DragLayout.java b/src/com/android/calculator2/DragLayout.java
new file mode 100644
index 0000000..3264b73
--- /dev/null
+++ b/src/com/android/calculator2/DragLayout.java
@@ -0,0 +1,358 @@
+/*
+ * 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.
+ */
+
+package com.android.calculator2;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.widget.ViewDragHelper;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public class DragLayout extends ViewGroup {
+
+    private static final double AUTO_OPEN_SPEED_LIMIT = 600.0;
+    private static final String KEY_IS_OPEN = "IS_OPEN";
+    private static final String KEY_SUPER_STATE = "SUPER_STATE";
+
+    private FrameLayout mHistoryFrame;
+    private ViewDragHelper mDragHelper;
+
+    // No concurrency; allow modifications while iterating.
+    private final List<DragCallback> mDragCallbacks = new CopyOnWriteArrayList<>();
+    private CloseCallback mCloseCallback;
+
+    private final Map<Integer, PointF> mLastMotionPoints = new HashMap<>();
+    private final Rect mHitRect = new Rect();
+
+    private int mVerticalRange;
+    private boolean mIsOpen;
+
+    public DragLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
+        mHistoryFrame = (FrameLayout) findViewById(R.id.history_frame);
+        super.onFinishInflate();
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        measureChildren(widthMeasureSpec, heightMeasureSpec);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        int displayHeight = 0;
+        for (DragCallback c : mDragCallbacks) {
+            displayHeight = Math.max(displayHeight, c.getDisplayHeight());
+        }
+        mVerticalRange = getHeight() - displayHeight;
+
+        final int childCount = getChildCount();
+        for (int i = 0; i < childCount; ++i) {
+            final View child = getChildAt(i);
+
+            int top = 0;
+            if (child == mHistoryFrame) {
+                if (mDragHelper.getCapturedView() == mHistoryFrame
+                        && mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE) {
+                    top = child.getTop();
+                } else {
+                    top = mIsOpen ? 0 : -mVerticalRange;
+                }
+            }
+            child.layout(0, top, child.getMeasuredWidth(), top + child.getMeasuredHeight());
+        }
+    }
+
+    @Override
+    protected Parcelable onSaveInstanceState() {
+        final Bundle bundle = new Bundle();
+        bundle.putParcelable(KEY_SUPER_STATE, super.onSaveInstanceState());
+        bundle.putBoolean(KEY_IS_OPEN, mIsOpen);
+        return bundle;
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Parcelable state) {
+        if (state instanceof Bundle) {
+            final Bundle bundle = (Bundle) state;
+            mIsOpen = bundle.getBoolean(KEY_IS_OPEN);
+            mHistoryFrame.setVisibility(mIsOpen ? View.VISIBLE : View.INVISIBLE);
+            for (DragCallback c : mDragCallbacks) {
+                c.onInstanceStateRestored(mIsOpen);
+            }
+
+            state = bundle.getParcelable(KEY_SUPER_STATE);
+        }
+        super.onRestoreInstanceState(state);
+    }
+
+    private void saveLastMotion(MotionEvent event) {
+        final int action = event.getActionMasked();
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+            case MotionEvent.ACTION_POINTER_DOWN: {
+                final int actionIndex = event.getActionIndex();
+                final int pointerId = event.getPointerId(actionIndex);
+                final PointF point = new PointF(event.getX(actionIndex), event.getY(actionIndex));
+                mLastMotionPoints.put(pointerId, point);
+                break;
+            }
+            case MotionEvent.ACTION_MOVE: {
+                for (int i = event.getPointerCount() - 1; i >= 0; --i) {
+                    final int pointerId = event.getPointerId(i);
+                    final PointF point = mLastMotionPoints.get(pointerId);
+                    if (point != null) {
+                        point.set(event.getX(i), event.getY(i));
+                    }
+                }
+                break;
+            }
+            case MotionEvent.ACTION_POINTER_UP: {
+                final int actionIndex = event.getActionIndex();
+                final int pointerId = event.getPointerId(actionIndex);
+                mLastMotionPoints.remove(pointerId);
+                break;
+            }
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL: {
+                mLastMotionPoints.clear();
+                break;
+            }
+        }
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent event) {
+        saveLastMotion(event);
+        return mDragHelper.shouldInterceptTouchEvent(event);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        // Workaround: do not process the error case where multi-touch would cause a crash.
+        if (event.getActionMasked() == MotionEvent.ACTION_MOVE
+                && mDragHelper.getViewDragState() == ViewDragHelper.STATE_DRAGGING
+                && mDragHelper.getActivePointerId() != ViewDragHelper.INVALID_POINTER
+                && event.findPointerIndex(mDragHelper.getActivePointerId()) == -1) {
+            mDragHelper.cancel();
+            return false;
+        }
+
+        saveLastMotion(event);
+
+        mDragHelper.processTouchEvent(event);
+        return true;
+    }
+
+    @Override
+    public void computeScroll() {
+        if (mDragHelper.continueSettling(true)) {
+            ViewCompat.postInvalidateOnAnimation(this);
+        }
+    }
+
+    private void onStartDragging() {
+        for (DragCallback c : mDragCallbacks) {
+            c.onStartDraggingOpen();
+        }
+        mHistoryFrame.setVisibility(VISIBLE);
+    }
+
+    public boolean isViewUnder(View view, int x, int y) {
+        view.getHitRect(mHitRect);
+        offsetDescendantRectToMyCoords((View) view.getParent(), mHitRect);
+        return mHitRect.contains(x, y);
+    }
+
+    public boolean isMoving() {
+        final int draggingState = mDragHelper.getViewDragState();
+        return draggingState == ViewDragHelper.STATE_DRAGGING
+                || draggingState == ViewDragHelper.STATE_SETTLING;
+    }
+
+    public boolean isOpen() {
+        return mIsOpen;
+    }
+
+    private void setClosed() {
+        if (mIsOpen) {
+            mIsOpen = false;
+            mHistoryFrame.setVisibility(View.INVISIBLE);
+
+            if (mCloseCallback != null) {
+                mCloseCallback.onClose();
+            }
+        }
+    }
+
+    public Animator createAnimator(boolean toOpen) {
+        if (mIsOpen == toOpen) {
+            return ValueAnimator.ofFloat(0f, 1f).setDuration(0L);
+        }
+
+        mIsOpen = toOpen;
+        mHistoryFrame.setVisibility(VISIBLE);
+
+        final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mDragHelper.cancel();
+                mDragHelper.smoothSlideViewTo(mHistoryFrame, 0, mIsOpen ? 0 : -mVerticalRange);
+            }
+        });
+
+        return animator;
+    }
+
+    public void setCloseCallback(CloseCallback callback) {
+        mCloseCallback = callback;
+    }
+
+    public void addDragCallback(DragCallback callback) {
+        mDragCallbacks.add(callback);
+    }
+
+    public void removeDragCallback(DragCallback callback) {
+        mDragCallbacks.remove(callback);
+    }
+
+    /**
+     * Callback when the layout is closed.
+     * We use this to pop the HistoryFragment off the backstack.
+     * We can't use a method in DragCallback because we get ConcurrentModificationExceptions on
+     * mDragCallbacks when executePendingTransactions() is called for popping the fragment off the
+     * backstack.
+     */
+    public interface CloseCallback {
+        void onClose();
+    }
+
+    /**
+     * Callbacks for coordinating with the RecyclerView or HistoryFragment.
+     */
+    public interface DragCallback {
+        // Callback when a drag to open begins.
+        void onStartDraggingOpen();
+
+        // Callback in onRestoreInstanceState.
+        void onInstanceStateRestored(boolean isOpen);
+
+        // Animate the RecyclerView text.
+        void whileDragging(float yFraction);
+
+        // Whether we should allow the view to be dragged.
+        boolean shouldCaptureView(View view, int x, int y);
+
+        int getDisplayHeight();
+    }
+
+    public class DragHelperCallback extends ViewDragHelper.Callback {
+        @Override
+        public void onViewDragStateChanged(int state) {
+            // The view stopped moving.
+            if (state == ViewDragHelper.STATE_IDLE
+                    && mDragHelper.getCapturedView().getTop() < -(mVerticalRange / 2)) {
+                setClosed();
+            }
+        }
+
+        @Override
+        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
+            for (DragCallback c : mDragCallbacks) {
+                // Top is between [-mVerticalRange, 0].
+                c.whileDragging(1f + (float) top / mVerticalRange);
+            }
+        }
+
+        @Override
+        public int getViewVerticalDragRange(View child) {
+            return mVerticalRange;
+        }
+
+        @Override
+        public boolean tryCaptureView(View view, int pointerId) {
+            final PointF point = mLastMotionPoints.get(pointerId);
+            if (point == null) {
+                return false;
+            }
+
+            final int x = (int) point.x;
+            final int y = (int) point.y;
+
+            for (DragCallback c : mDragCallbacks) {
+                if (!c.shouldCaptureView(view, x, y)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        @Override
+        public int clampViewPositionVertical(View child, int top, int dy) {
+            return Math.max(Math.min(top, 0), -mVerticalRange);
+        }
+
+        @Override
+        public void onViewCaptured(View capturedChild, int activePointerId) {
+            super.onViewCaptured(capturedChild, activePointerId);
+
+            if (!mIsOpen) {
+                mIsOpen = true;
+                onStartDragging();
+            }
+        }
+
+        @Override
+        public void onViewReleased(View releasedChild, float xvel, float yvel) {
+            final boolean settleToOpen;
+            if (yvel > AUTO_OPEN_SPEED_LIMIT) {
+                // Speed has priority over position.
+                settleToOpen = true;
+            } else if (yvel < -AUTO_OPEN_SPEED_LIMIT) {
+                settleToOpen = false;
+            } else {
+                settleToOpen = releasedChild.getTop() > -(mVerticalRange / 2);
+            }
+
+            if (mDragHelper.settleCapturedViewAt(0, settleToOpen ? 0 : -mVerticalRange)) {
+                ViewCompat.postInvalidateOnAnimation(DragLayout.this);
+            }
+        }
+    }
+}
diff --git a/src/com/android/calculator2/Evaluator.java b/src/com/android/calculator2/Evaluator.java
index 33960ba..655aa70 100644
--- a/src/com/android/calculator2/Evaluator.java
+++ b/src/com/android/calculator2/Evaluator.java
@@ -16,52 +16,59 @@
 
 package com.android.calculator2;
 
-import android.app.AlertDialog;
 import android.content.Context;
-import android.content.DialogInterface;
 import android.content.SharedPreferences;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Handler;
 import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.StringRes;
 import android.support.annotation.VisibleForTesting;
+import android.text.Spannable;
 import android.util.Log;
 
-import com.hp.creals.CR;  // For exception classes.
+import com.hp.creals.CR;
 
+import java.io.ByteArrayInputStream;
 import java.io.DataInput;
+import java.io.DataInputStream;
 import java.io.DataOutput;
 import java.io.IOException;
-import java.math.BigInteger;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.Random;
 import java.util.TimeZone;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicReference;
 
 /**
- * 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
@@ -88,15 +95,132 @@
  * We ensure that only one evaluation of either kind (AsyncEvaluator or AsyncReevaluator) is
  * running at a time.
  */
-class Evaluator {
+public class Evaluator implements CalculatorExpr.ExprResolver {
+
+    private static Evaluator evaluator;
+
+    public static String TIMEOUT_DIALOG_TAG = "timeout";
+
+    @NonNull
+    public static Evaluator getInstance(Context context) {
+        if (evaluator == null) {
+            evaluator = new Evaluator(context.getApplicationContext());
+        }
+        return evaluator;
+    }
+
+    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.
+         * Returns zero if metrics information is not yet available.
+         */
+        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 main 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.
+    // Each expression index can only have one outstanding evaluation request at a time.
+    // To avoid conflicts between the history and main View, we copy the main expression
+    // to allow independent evaluation by both.
+    public static final long HISTORY_MAIN_INDEX = -1;  // Read-only copy of main expression.
+    // 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
@@ -123,77 +247,155 @@
 
     // The largest number of digits to the right of the decimal point to which we will evaluate to
     // compute proper scientific notation for values close to zero.  Chosen to ensure that we
-    // always to better than IEEE double precision at identifying nonzeros.
-    // This used only when we cannot a prior determine the most significant digit position, as
+    // always to better than IEEE double precision at identifying nonzeros. And then some.
+    // This is used only when we cannot a priori determine the most significant digit position, as
     // we always can if we have a rational representation.
-    private static final int MAX_MSD_PREC_OFFSET = 320;
+    private static final int MAX_MSD_PREC_OFFSET = 1100;
 
     // If we can replace an exponent by this many leading zeroes, we do so.  Also used in
     // estimating exponent size for truncating short representation.
     private static final int EXP_COST = 3;
 
-    private final Calculator mCalculator;
-    private final CalculatorResult mResult;
+    // Listener that reports changes to the state (empty/filled) of memory. Protected for testing.
+    private Callback mCallback;
 
-    // The current caluclator expression.
-    private CalculatorExpr mExpr;
-
-    // Last saved expression.  Either null or contains a single CalculatorExpr.PreEval node.
-    private CalculatorExpr mSaved;
+    // Context for database helper.
+    private Context mContext;
 
     //  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.
+    // Used to represent an erroneous result or a required evaluation. Not displayed.
+    private static final String ERRONEOUS_RESULT = "ERR";
+
+    /**
+     * An individual CalculatorExpr, together with its evaluation state.
+     * Only the main expression may be changed in-place. The HISTORY_MAIN_INDEX expression is
+     * periodically reset to be a fresh immutable copy of the main expression.
+     * 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 either an AsyncEvaluator
+        // (if mResultString == null or it's obsolete), or an AsyncReevaluator.
+        // We arrange that only one evaluator is active at a time, in part by maintaining
+        // two separate ExprInfo structure for the main and history view, so that they can
+        // arrange for independent evaluators.
+        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.
+        // ERRONEOUS_RESULT indicates evaluation resulted in an error.
+        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;
+    }
+
+    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(Context context) {
+        mContext = context;
+        setMainExpr(new ExprInfo(new CalculatorExpr(), false));
         mSavedName = "none";
         mTimeoutHandler = new Handler();
 
-        mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(calculator);
-        mDegreeMode = mSharedPrefs.getBoolean(KEY_PREF_DEGREE_MODE, false);
+        mExprDB = new ExpressionDB(context);
+        mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
+        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 && savedIndex != -1 /* Recover from old corruption */) {
+            setSavedIndexWhenEvaluated(savedIndex);
+        }
+        if (memoryIndex != 0 && memoryIndex != -1) {
+            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();
+    }
+
+    /**
+     * Set the Callback for showing dialogs and notifying the UI about memory state changes.
+     * @param callback
+     */
+    public void setCallback(Callback callback) {
+        mCallback = callback;
+    }
+
+    /**
+     * Does the expression index refer to a transient and mutable expression?
+     */
+    private boolean isMutableIndex(long index) {
+        return index == MAIN_INDEX || index == HISTORY_MAIN_INDEX;
     }
 
     /**
@@ -226,14 +428,9 @@
     }
 
     private void displayCancelledMessage() {
-        new AlertDialog.Builder(mCalculator)
-            .setMessage(R.string.cancelled)
-            .setPositiveButton(R.string.dismiss,
-                new DialogInterface.OnClickListener() {
-                    public void onClick(DialogInterface d, int which) { }
-                })
-            .create()
-            .show();
+        if (mCallback != null) {
+            mCallback.showMessageDialog(0, R.string.cancelled, 0, null);
+        }
     }
 
     // Timeout handling.
@@ -243,16 +440,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
      */
@@ -267,27 +454,39 @@
      * @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;
+
+    /**
+     * Timeout for non-MAIN expressions. Note that there may be many such evaluations in
+     * progress on the same thread or core. Thus the evaluation latency may include that needed
+     * to complete previously enqueued evaluations. Thus the longTimeout flag is not very
+     * meaningful, and currently ignored.
+     * Since this is only used for expressions that we have previously successfully evaluated,
+     * these timeouts hsould never trigger.
+     */
+    private static final long NON_MAIN_TIMEOUT = 100000;
 
     /**
      * 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 = 150000;
 
-    private void displayTimeoutMessage() {
-        AlertDialogFragment.showMessageDialog(mCalculator, mCalculator.getString(R.string.timeout),
-                (mLongTimeout ? null : mCalculator.getString(R.string.ok_remove_timeout)));
+    private void displayTimeoutMessage(boolean longTimeout) {
+        if (mCallback != null) {
+            mCallback.showMessageDialog(R.string.dialog_timeout, R.string.timeout,
+                    longTimeout ? 0 : R.string.ok_remove_timeout, TIMEOUT_DIALOG_TAG);
+        }
     }
 
-    public void setLongTimeOut() {
-        mLongTimeout = true;
+    public void setLongTimeout() {
+        mMainExpr.mLongTimeout = true;
     }
 
     /**
@@ -298,52 +497,94 @@
      */
     class AsyncEvaluator extends AsyncTask<Void, Void, InitialResult> {
         private boolean mDm;  // degrees
-        private boolean mRequired; // Result was requested by user.
+        public 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 = NON_MAIN_TIMEOUT;
+            }
             mTimeoutRunnable = new Runnable() {
                 @Override
                 public void run() {
-                    handleTimeOut();
+                    handleTimeout();
                 }
             };
+            mTimeoutHandler.removeCallbacks(mTimeoutRunnable);
             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;
+            final 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) {
+                    try {
+                        res = mExprInfo.mExpr.eval(mDm, Evaluator.this);
+                        if (isCancelled()) {
+                            // TODO: This remains very slightly racey. Fix this.
+                            throw new CR.AbortedException();
+                        }
+                        res = putResultIfAbsent(mIndex, res);
+                    } catch (StackOverflowError e) {
+                        // Absurdly large integer exponents can cause this. There might be other
+                        // examples as well. Treat it as a timeout.
+                        return new InitialResult(R.string.timeout);
+                    }
+                }
                 if (isTooBig(res)) {
                     // Avoid starting a long uninterruptible decimal conversion.
                     return new InitialResult(R.string.timeout);
@@ -370,7 +611,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;
@@ -390,50 +632,59 @@
                 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);
+                    if (mRequired) {
+                        mExprInfo.mResultString = ERRONEOUS_RESULT;
+                    }
+                    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 asynchronously 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;
         }
@@ -451,7 +702,7 @@
      * but we have failed to prove there aren't such cases.
      */
     @VisibleForTesting
-    static String unflipZeroes(String oldDigs, int oldPrecOffset, String newDigs,
+    public static String unflipZeroes(String oldDigs, int oldPrecOffset, String newDigs,
             int newPrecOffset) {
         final int oldLen = oldDigs.length();
         if (oldDigs.charAt(oldLen - 1) != '9') {
@@ -487,13 +738,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) {
@@ -511,39 +775,44 @@
                 // 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);
+                mExprInfo.mResultString = ERRONEOUS_RESULT;
+                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.
+     * Only called if prior evaluation succeeded.
      */
-    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);
     }
 
     /**
@@ -555,7 +824,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) {
@@ -579,12 +848,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;
@@ -646,7 +916,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
@@ -741,25 +1011,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;
     }
@@ -772,7 +1043,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.
@@ -783,32 +1054,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;
@@ -817,19 +1091,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.
@@ -841,135 +1115,225 @@
     }
 
     /**
-     * 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();
+    public void clearMain() {
+        mMainExpr.mExpr.clear();
         mHasTrigFuncs = false;
-        clearCache();
+        clearMainCache();
+        mMainExpr.mLongTimeout = false;
     }
 
-    public void clear() {
-        clearPreservingTimeout();
-        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;
+    private void evaluateResult(long index, EvaluationListener listener, CharMetricsInfo cmi,
+            boolean required) {
+        ExprInfo ei = mExprs.get(index);
+        if (index == MAIN_INDEX) {
+            clearMainCache();
+        }  // Otherwise the expression is immutable.
+        AsyncEvaluator eval =  new AsyncEvaluator(index, listener, cmi, ei.mDegreeMode, required);
+        ei.mEvaluator = eval;
+        eval.execute();
+        if (index == MAIN_INDEX) {
+            mChangedValue = false;
+        }
     }
 
     /**
-     * Start optional evaluation of result and display when ready.
-     * Can quietly time out without a user-visible display.
+     * Notify listener of a previously completed evaluation.
      */
-    public void evaluateAndShowResult() {
-        if (!mChangedValue) {
-            // Already done or in progress.
+    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.
+     * No-op if cmi.getMaxChars() == 0.
+     */
+    public void evaluateAndNotify(long index, EvaluationListener listener, CharMetricsInfo cmi) {
+        if (cmi.getMaxChars() == 0) {
+            // Probably shouldn't happen. If it does, we didn't promise to do anything anyway.
             return;
         }
-        // In very odd cases, there can be significant latency to evaluate.
-        // Don't show obsolete result.
-        mResult.clear();
-        evaluateResult(false);
+        ExprInfo ei = ensureExprIsCached(index);
+        if (ei.mResultString != null && ei.mResultString != ERRONEOUS_RESULT
+                && !(index == MAIN_INDEX && mChangedValue)) {
+            // Already done. Just notify.
+            notifyImmediately(MAIN_INDEX, mMainExpr, listener, cmi);
+            return;
+        } else if (ei.mEvaluator != null) {
+            // We only allow a single listener per expression, so this request must be redundant.
+            return;
+        }
+        evaluateResult(index, listener, cmi, 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.
+     * Start required evaluation of expression at given index and call back listener when ready.
+     * If index is MAIN_INDEX, we may also directly display a timeout message.
+     * Uses longer timeouts than optional evaluation.
+     * Requires cmi.getMaxChars() != 0.
      */
-    public void requireResult() {
-        if (mResultString == null || mChangedValue) {
-            // Restart evaluator in requested mode, i.e. with longer timeout.
-            cancelAll(true);
-            evaluateResult(true);
-        } 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);
+    public void requireResult(long index, EvaluationListener listener, CharMetricsInfo cmi) {
+        if (cmi.getMaxChars() == 0) {
+            throw new AssertionError("requireResult called too early");
         }
+        ExprInfo ei = ensureExprIsCached(index);
+        if (ei.mResultString == null || (index == MAIN_INDEX && mChangedValue)) {
+            if (index == HISTORY_MAIN_INDEX) {
+                // We don't want to compute a result for HISTORY_MAIN_INDEX that was
+                // not already computed for the main expression. Pretend we timed out.
+                // The error case doesn't get here.
+                listener.onCancelled(index);
+            } else if ((ei.mEvaluator instanceof AsyncEvaluator)
+                    && ((AsyncEvaluator)(ei.mEvaluator)).mRequired) {
+                // Duplicate request; ignore.
+            } else {
+                // (Re)start evaluator in requested mode, i.e. with longer timeout.
+                cancel(ei, true);
+                evaluateResult(index, listener, cmi, true);
+            }
+        } else if (ei.mResultString == ERRONEOUS_RESULT) {
+            // Just re-evaluate to generate a new notification.
+            cancel(ei, true);
+            evaluateResult(index, listener, cmi, true);
+        } else {
+            notifyImmediately(index, ei, listener, cmi);
+        }
+    }
+
+    /**
+     * Whether this expression has explicitly been evaluated (User pressed "=")
+     */
+    public boolean hasResult(long index) {
+        final ExprInfo ei = ensureExprIsCached(index);
+        return ei.mResultString != null;
     }
 
     /**
      * 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.
+     * Cancel any current background task associated with the given ExprInfo.
      * @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) {
+            if (quiet && (expr.mEvaluator instanceof AsyncEvaluator)) {
+                ((AsyncEvaluator)(expr.mEvaluator)).suppressCancelMessage();
             }
-            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;
+            // Reevaluation in progress.
+            if (expr.mVal.get() != null) {
+                expr.mEvaluator.cancel(true);
+                expr.mResultStringOffsetReq = expr.mResultStringOffset;
+                // Backgound computation touches only constructive reals.
+                // OK not to wait.
+                expr.mEvaluator = null;
+            } else {
+                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;
+            }
         }
         return false;
     }
 
     /**
-     * Restore the evaluator state, including the expression and any saved value.
+     * Cancel any current background task associated with the given ExprInfo.
+     * @param quiet suppress cancellation message
+     * @return true if we cancelled an initial evaluation
+     */
+    public boolean cancel(long index, boolean quiet)
+    {
+        ExprInfo ei = mExprs.get(index);
+        if (ei == null) {
+            return false;
+        } else {
+            return cancel(ei, quiet);
+        }
+    }
+
+    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);
+        }
+    }
+
+    /**
+     * Quietly cancel all evaluations associated with expressions other than the main one.
+     * These are currently the evaluations associated with the history fragment.
+     */
+    public void cancelNonMain() {
+        // TODO: May want to keep active evaluators in a HashSet to avoid traversing
+        // all expressions we've looked at.
+        for (ExprInfo expr: mExprs.values()) {
+            if (expr != mMainExpr) {
+                cancel(expr, true);
+            }
+        }
+    }
+
+    /**
+     * Restore the evaluator state, including the current expression.
      */
     public void restoreInstanceState(DataInput in) {
         mChangedValue = true;
         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);
         }
@@ -980,13 +1344,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);
         }
@@ -994,7 +1354,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
@@ -1005,7 +1365,7 @@
             return true;
         } else {
             mChangedValue = mChangedValue || !KeyMaps.isBinary(id);
-            if (mExpr.add(id)) {
+            if (mMainExpr.mExpr.add(id)) {
                 if (!mHasTrigFuncs) {
                     mHasTrigFuncs = KeyMaps.isTrigFunc(id);
                 }
@@ -1016,49 +1376,190 @@
         }
     }
 
+    /**
+     * 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 expression with the given index.
+     * We remove trailing binary operators in the copy.
+     * mTimeStamp is not copied.
      */
-    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.
+     * index1 should correspond to an immutable expression, and should thus NOT
+     * be MAIN_INDEX. Index2 may be MAIN_INDEX. Both expressions are presumed
+     * to have been evaluated.  The result is unevaluated.
+     * Can return null if evaluation resulted in an error (a very unlikely case).
+     */
+    private ExprInfo sum(long index1, long index2) {
+        return generalized_sum(index1, index2, R.id.op_add);
+    }
+
+    /**
+     * Return an ExprInfo corresponding to the subtraction of the value at the subtrahend index
+     * from value at the minuend index (minuend - subtrahend = result). Both are presumed to have
+     * been previously evaluated. The result is unevaluated. Can return null.
+     */
+    private ExprInfo difference(long minuendIndex, long subtrahendIndex) {
+        return generalized_sum(minuendIndex, subtrahendIndex, R.id.op_sub);
+    }
+
+    private ExprInfo generalized_sum(long index1, long index2, int op) {
+        // TODO: Consider not collapsing expr2, to save database space.
+        // Note that this is a bit tricky, since our expressions can contain unbalanced lparens.
+        CalculatorExpr result = new CalculatorExpr();
+        CalculatorExpr collapsed1 = getCollapsedExpr(index1);
+        CalculatorExpr collapsed2 = getCollapsedExpr(index2);
+        if (collapsed1 == null || collapsed2 == null) {
+            return null;
+        }
+        result.append(collapsed1);
+        result.add(op);
+        result.append(collapsed2);
+        ExprInfo resultEi = new ExprInfo(result, false /* dont care about degrees/radians */);
+        resultEi.mLongTimeout = mExprs.get(index1).mLongTimeout
+                || mExprs.get(index2).mLongTimeout;
+        return resultEi;
+    }
+
+    /**
+     * Add the expression described by the argument to the database.
+     * Returns the new row id in the database.
+     * Fills in timestamp in ei, if it was not previously set.
+     * 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) {
+        byte[] serializedExpr = ei.mExpr.toBytes();
+        ExpressionDB.RowData rd = new ExpressionDB.RowData(serializedExpr, ei.mDegreeMode,
+                ei.mLongTimeout, 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;
+        if (resultIndex == MAIN_INDEX) {
+            throw new AssertionError("Should not store main expression");
+        }
+        mExprs.put(resultIndex, ei);
+        return resultIndex;
+    }
+
+    /**
+     * Preserve a copy of the expression at old_index at a new index.
+     * This is useful only of old_index is MAIN_INDEX or HISTORY_MAIN_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(long old_index, boolean in_history) {
+        ExprInfo ei = copy(old_index, true);
+        if (ei.mResultString == null || ei.mResultString == ERRONEOUS_RESULT) {
+            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 the database, but may have been lost from the cache.
+     */
+    public void represerve() {
+        long resultIndex = getMaxIndex();
+        // This requires database access only if the local state was preserved, but we
+        // recreated the Evaluator.  That excludes the common cases of device rotation, etc.
+        // TODO: Revisit once we deal with database failures. We could just copy from
+        // MAIN_INDEX instead, but that loses the timestamp.
+        ensureExprIsCached(resultIndex);
+    }
+
+    /**
+     * Discard previous expression in HISTORY_MAIN_INDEX and replace it by a fresh copy
+     * of the main expression. Note that the HISTORY_MAIN_INDEX expresssion is not preserved
+     * in the database or anywhere else; it is always reconstructed when needed.
+     */
+    public void copyMainToHistory() {
+        cancel(HISTORY_MAIN_INDEX, true /* quiet */);
+        ExprInfo ei = copy(MAIN_INDEX, true);
+        mExprs.put(HISTORY_MAIN_INDEX, 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 = isMutableIndex(index) ? preserve(index, false) : index;
+        final ExprInfo ei = mExprs.get(real_index);
+        final String rs = ei.mResultString;
+        // An error can occur here only under extremely unlikely conditions.
+        // Check anyway, and just refuse.
+        // rs *should* never be null, but it happens. Check as a workaround to protect against
+        // crashes until we find the root cause (b/34801142)
+        if (rs == ERRONEOUS_RESULT || rs == null) {
+            return null;
+        }
+        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 boolean longTimeout = mExprs.get(index).mLongTimeout;
+        final CalculatorExpr abbrvExpr = getCollapsedExpr(index);
+        clearMain();
+        mMainExpr.mExpr.append(abbrvExpr);
+        mMainExpr.mLongTimeout = longTimeout;
         mChangedValue = true;
         mHasTrigFuncs = false;  // Degree mode no longer affects expression value.
     }
@@ -1070,21 +1571,158 @@
         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();
+
+        if (mCallback != null) {
+            mCallback.onMemoryStateChanged();
+        }
+    }
+
+    /**
+     * 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
+                || mExprs.get(index).mResultString == ERRONEOUS_RESULT) {
             return false;
         }
-        final CalculatorExpr abbrvExpr = getResultExpr();
-        mSaved.clear();
-        mSaved.append(abbrvExpr);
-        mLongSavedTimeout = mLongTimeout;
+        setSavedIndex(isMutableIndex(index) ? preserve(index, 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(isMutableIndex(index) ? preserve(index, 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(mMemoryIndex, index);
+        if (newEi != null) {
+            long newIndex = addToDB(false, newEi);
+            mMemoryIndex = 0;  // Invalidate while we're evaluating.
+            setMemoryIndexWhenEvaluated(newIndex, true /* persist */);
+        }
+    }
+
+    /**
+     * Save an an expression representing the subtraction of the expression with the given index
+     * from "memory." Make mMemoryIndex point to it when we complete evaluating.
+     */
+    public void subtractFromMemory(long index) {
+        ExprInfo newEi = difference(mMemoryIndex, index);
+        if (newEi != null) {
+            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)
@@ -1092,12 +1730,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();
@@ -1106,21 +1743,31 @@
         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) {
+        ExprInfo ei = mExprs.get(index);
+        mChangedValue = true;
+        mMainExpr.mLongTimeout |= ei.mLongTimeout;
+        CalculatorExpr collapsed = getCollapsedExpr(index);
+        if (collapsed != null) {
+            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() {
@@ -1128,21 +1775,70 @@
         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;
+        }
+        if (index == MAIN_INDEX) {
+            throw new AssertionError("Main expression should be cached");
+        }
+        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.mLongTimeout = row.longTimeout();
+        } 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;
+    }
+
+    @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();
+        }
     }
 
     /**
@@ -1206,7 +1902,62 @@
         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 getExprAsSpannable(index).toString();
+    }
+
+    public Spannable getExprAsSpannable(long index) {
+        return getExpr(index).toSpannableStringBuilder(mContext);
+    }
+
+    /**
+     * 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();
+    }
+
+    /**
+     * Wait for pending writes to the database to complete.
+     */
+    public void waitForWrites() {
+        mExprDB.waitForWrites();
+    }
+
+    /**
+     * Destroy the current evaluator, forcing getEvaluator to allocate a new one.
+     * This is needed for testing, since Robolectric apparently doesn't let us preserve
+     * an open databse across tests. Cf. https://github.com/robolectric/robolectric/issues/1890 .
+     */
+    public void destroyEvaluator() {
+        mExprDB.close();
+        evaluator = null;
+    }
+
+    public interface Callback {
+        void onMemoryStateChanged();
+        void showMessageDialog(@StringRes int title, @StringRes int message,
+                @StringRes int positiveButtonLabel, String tag);
+    }
 }
diff --git a/src/com/android/calculator2/ExpressionDB.java b/src/com/android/calculator2/ExpressionDB.java
new file mode 100644
index 0000000..9a0f8ec
--- /dev/null
+++ b/src/com/android/calculator2/ExpressionDB.java
@@ -0,0 +1,619 @@
+/*
+ * 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.
+ */
+
+// We make some strong assumptions about the databases we manipulate.
+// We maintain a single table containg expressions, their indices in the sequence of
+// expressions, and some data associated with each expression.
+// All indices are used, except for a small gap around zero.  New rows are added
+// either just below the current minimum (negative) index, or just above the current
+// maximum index. Currently no rows are deleted unless we clear the whole table.
+
+// 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.
+// TODO: We may want to switch to a scheme in which all expressions saved in the database have
+// a positive index, and a flag indicates whether the expression is displayed as part of
+// the history or not. That would avoid potential thrashing between CursorWindows when accessing
+// with a negative index. It would also make it easy to sort expressions in dependency order,
+// which helps with avoiding deep recursion during evaluation. But it makes the history UI
+// implementation more complicated. It should be possible to make this change without a
+// database version bump.
+
+// This ensures strong thread-safety, i.e. each call looks atomic to other threads. We need some
+// such property, since expressions may be read by one thread while the main thread is updating
+// another expression.
+
+package com.android.calculator2;
+
+import android.app.Activity;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.AbstractWindowedCursor;
+import android.database.Cursor;
+import android.database.CursorWindow;
+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 android.view.View;
+
+public class ExpressionDB {
+    private final boolean CONTINUE_WITH_BAD_DB = false;
+
+    /* 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";
+    }
+
+    /* 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.
+        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 RowData(byte[] expr, int flags, long timeStamp) {
+            mExpression = expr;
+            mFlags = flags;
+            mTimeStamp = timeStamp;
+        }
+        /**
+         * 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) {
+            this(expr, flagsFromDegreeAndTimeout(degreeMode, longTimeout), timeStamp);
+        }
+        public boolean degreeMode() {
+            return degreeModeFromFlags(mFlags);
+        }
+        public boolean longTimeout() {
+            return longTimeoutFromFlags(mFlags);
+        }
+        /**
+         * 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();
+            }
+            cvs.put(ExpressionEntry.COLUMN_NAME_TIMESTAMP, mTimeStamp);
+            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)";
+    private static final String SQL_DROP_TABLE =
+            "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 static final String SQL_GET_ALL = "SELECT * FROM " + ExpressionEntry.TABLE_NAME
+            + " WHERE " + ExpressionEntry._ID + " <= ? AND " +
+            ExpressionEntry._ID +  " >= ?" + " ORDER BY " + ExpressionEntry._ID + " DESC ";
+    // We may eventually need an index by timestamp. We don't use it yet.
+    private static final String SQL_CREATE_TIMESTAMP_INDEX =
+            "CREATE INDEX timestamp_index ON " + ExpressionEntry.TABLE_NAME + "("
+            + ExpressionEntry.COLUMN_NAME_TIMESTAMP + ")";
+    private static final String SQL_DROP_TIMESTAMP_INDEX = "DROP INDEX IF EXISTS timestamp_index";
+
+    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);
+            db.execSQL(SQL_CREATE_TIMESTAMP_INDEX);
+        }
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            // For now just throw away history on database version upgrade/downgrade.
+            db.execSQL(SQL_DROP_TIMESTAMP_INDEX);
+            db.execSQL(SQL_DROP_TABLE);
+            onCreate(db);
+        }
+        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            onUpgrade(db, oldVersion, newVersion);
+        }
+    }
+
+    private ExpressionDBHelper mExpressionDBHelper;
+
+    private SQLiteDatabase mExpressionDB;  // Constant after initialization.
+
+    // Expression indices between mMinAccessible and mMaxAccessible inclusive can be accessed.
+    // We set these to more interesting values if a database access fails.
+    // We punt on writes outside this range. We should never read outside this range.
+    // If higher layers refer to an index outside this range, it will already be cached.
+    // This also somewhat limits the size of the database, but only to an unreasonably
+    // huge value.
+    private long mMinAccessible = -10000000L;
+    private long mMaxAccessible = 10000000L;
+
+    // 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;
+
+    // A cursor that refers to the whole table, in reverse order.
+    private AbstractWindowedCursor mAllCursor;
+
+    // Expression index corresponding to a zero absolute offset for mAllCursor.
+    // This is the argument we passed to the query.
+    // We explicitly query only for entries that existed when we started, to avoid
+    // interference from updates as we're running. It's unclear whether or not this matters.
+    private int mAllCursorBase;
+
+    // Database has been opened, mMinIndex and mMaxIndex are correct, mAllCursorBase and
+    // mAllCursor have been set.
+    private boolean mDBInitialized;
+
+    // Gap between negative and positive row ids in the database.
+    // Expressions with index [MAXIMUM_MIN_INDEX .. 0] are not stored.
+    private static final long GAP = -MAXIMUM_MIN_INDEX + 1;
+
+    // mLock protects mExpressionDB, mMinAccessible, and mMaxAccessible, mAllCursor,
+    // mAllCursorBase, mMinIndex, mMaxIndex, and mDBInitialized. We access mExpressionDB without
+    // synchronization after it's known to be initialized.  Used to wait for database
+    // initialization.
+    private Object mLock = new Object();
+
+    public ExpressionDB(Context context) {
+        mExpressionDBHelper = new ExpressionDBHelper(context);
+        AsyncInitializer initializer = new AsyncInitializer();
+        // All calls that create background database accesses are made from the UI thread, and
+        // use a SERIAL_EXECUTOR. Thus they execute in order.
+        initializer.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, mExpressionDBHelper);
+    }
+
+    // Is database completely unusable?
+    private boolean isDBBad() {
+        if (!CONTINUE_WITH_BAD_DB) {
+            return false;
+        }
+        synchronized(mLock) {
+            return mMinAccessible > mMaxAccessible;
+        }
+    }
+
+    // Is the index in the accessible range of the database?
+    private boolean inAccessibleRange(long index) {
+        if (!CONTINUE_WITH_BAD_DB) {
+            return true;
+        }
+        synchronized(mLock) {
+            return index >= mMinAccessible && index <= mMaxAccessible;
+        }
+    }
+
+
+    private void setBadDB() {
+        if (!CONTINUE_WITH_BAD_DB) {
+            Log.e("Calculator", "Database access failed");
+            throw new RuntimeException("Database access failed");
+        }
+        displayDatabaseWarning();
+        synchronized(mLock) {
+            mMinAccessible = 1L;
+            mMaxAccessible = -1L;
+        }
+    }
+
+    /**
+     * Initialize the database in the background.
+     */
+    private class AsyncInitializer extends AsyncTask<ExpressionDBHelper, Void, SQLiteDatabase> {
+        @Override
+        protected SQLiteDatabase doInBackground(ExpressionDBHelper... helper) {
+            try {
+                SQLiteDatabase db = helper[0].getWritableDatabase();
+                synchronized(mLock) {
+                    mExpressionDB = db;
+                    try (Cursor minResult = db.rawQuery(SQL_GET_MIN, null)) {
+                        if (!minResult.moveToFirst()) {
+                            // Empty database.
+                            mMinIndex = MAXIMUM_MIN_INDEX;
+                        } else {
+                            mMinIndex = Math.min(minResult.getLong(0), MAXIMUM_MIN_INDEX);
+                        }
+                    }
+                    try (Cursor maxResult = db.rawQuery(SQL_GET_MAX, null)) {
+                        if (!maxResult.moveToFirst()) {
+                            // Empty database.
+                            mMaxIndex = 0L;
+                        } else {
+                            mMaxIndex = Math.max(maxResult.getLong(0), 0L);
+                        }
+                    }
+                    if (mMaxIndex > Integer.MAX_VALUE) {
+                        throw new AssertionError("Expression index absurdly large");
+                    }
+                    mAllCursorBase = (int)mMaxIndex;
+                    if (mMaxIndex != 0L || mMinIndex != MAXIMUM_MIN_INDEX) {
+                        // Set up a cursor for reading the entire database.
+                        String args[] = new String[]
+                                { Long.toString(mAllCursorBase), Long.toString(mMinIndex) };
+                        mAllCursor = (AbstractWindowedCursor) db.rawQuery(SQL_GET_ALL, args);
+                        if (!mAllCursor.moveToFirst()) {
+                            setBadDB();
+                            return null;
+                        }
+                    }
+                    mDBInitialized = true;
+                    // We notify here, since there are unlikely cases in which the UI thread
+                    // may be blocked on us, preventing onPostExecute from running.
+                    mLock.notifyAll();
+                }
+                return db;
+            } catch(SQLiteException e) {
+                Log.e("Calculator", "Database initialization failed.\n", e);
+                synchronized(mLock) {
+                    setBadDB();
+                    mLock.notifyAll();
+                }
+                return null;
+            }
+        }
+
+        @Override
+        protected void onPostExecute(SQLiteDatabase result) {
+            if (result == null) {
+                displayDatabaseWarning();
+            } // else doInBackground already set expressionDB.
+        }
+        // On cancellation we do nothing;
+    }
+
+    private boolean databaseWarningIssued;
+
+    /**
+     * Display a warning message that a database access failed.
+     * Do this only once. TODO: Replace with a real UI message.
+     */
+    void displayDatabaseWarning() {
+        if (!databaseWarningIssued) {
+            Log.e("Calculator", "Calculator restarting due to database error");
+            databaseWarningIssued = true;
+        }
+    }
+
+    /**
+     * Wait until the database and mAllCursor, etc. have been initialized.
+     */
+    private void waitForDBInitialized() {
+        synchronized(mLock) {
+            // InterruptedExceptions are inconvenient here. Defer.
+            boolean caught = false;
+            while (!mDBInitialized && !isDBBad()) {
+                try {
+                    mLock.wait();
+                } catch(InterruptedException e) {
+                    caught = true;
+                }
+            }
+            if (caught) {
+                Thread.currentThread().interrupt();
+            }
+        }
+    }
+
+    /**
+     * Erase the entire database. Assumes no other accesses to the database are
+     * currently in progress
+     * These tasks must be executed on a serial executor to avoid reordering writes.
+     */
+    private class AsyncEraser extends AsyncTask<Void, Void, Void> {
+        @Override
+        protected Void doInBackground(Void... nothings) {
+            mExpressionDB.execSQL(SQL_DROP_TIMESTAMP_INDEX);
+            mExpressionDB.execSQL(SQL_DROP_TABLE);
+            try {
+                mExpressionDB.execSQL("VACUUM");
+            } catch(Exception e) {
+                Log.v("Calculator", "Database VACUUM failed\n", e);
+                // Should only happen with concurrent execution, which should be impossible.
+            }
+            mExpressionDB.execSQL(SQL_CREATE_ENTRIES);
+            mExpressionDB.execSQL(SQL_CREATE_TIMESTAMP_INDEX);
+            return null;
+        }
+        @Override
+        protected void onPostExecute(Void nothing) {
+            synchronized(mLock) {
+                // Reinitialize everything to an empty and fully functional database.
+                mMinAccessible = -10000000L;
+                mMaxAccessible = 10000000L;
+                mMinIndex = MAXIMUM_MIN_INDEX;
+                mMaxIndex = mAllCursorBase = 0;
+                mDBInitialized = true;
+                mLock.notifyAll();
+            }
+        }
+        // On cancellation we do nothing;
+    }
+
+    /**
+     * Erase ALL database entries.
+     * This is currently only safe if expressions that may refer to them are also erased.
+     * Should only be called when concurrent references to the database are impossible.
+     * TODO: Look at ways to more selectively clear the database.
+     */
+    public void eraseAll() {
+        waitForDBInitialized();
+        synchronized(mLock) {
+            mDBInitialized = false;
+        }
+        AsyncEraser eraser = new AsyncEraser();
+        eraser.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
+    }
+
+    // We track the number of outstanding writes to prevent onSaveInstanceState from
+    // completing with in-flight database writes.
+
+    private int mIncompleteWrites = 0;
+    private Object mWriteCountsLock = new Object();  // Protects the preceding field.
+
+    private void writeCompleted() {
+        synchronized(mWriteCountsLock) {
+            if (--mIncompleteWrites == 0) {
+                mWriteCountsLock.notifyAll();
+            }
+        }
+    }
+
+    private void writeStarted() {
+        synchronized(mWriteCountsLock) {
+            ++mIncompleteWrites;
+        }
+    }
+
+    /**
+     * Wait for in-flight writes to complete.
+     * This is not safe to call from one of our background tasks, since the writing
+     * tasks may be waiting for the same underlying thread that we're using, resulting
+     * in deadlock.
+     */
+    public void waitForWrites() {
+        synchronized(mWriteCountsLock) {
+            boolean caught = false;
+            while (mIncompleteWrites != 0) {
+                try {
+                    mWriteCountsLock.wait();
+                } catch (InterruptedException e) {
+                    caught = true;
+                }
+            }
+            if (caught) {
+                Thread.currentThread().interrupt();
+            }
+        }
+    }
+
+    /**
+     * Insert the given row in the database without blocking the UI thread.
+     * These tasks must be executed on a serial executor to avoid reordering writes.
+     */
+    private class AsyncWriter extends AsyncTask<ContentValues, Void, Long> {
+        @Override
+        protected Long doInBackground(ContentValues... cvs) {
+            long index = cvs[0].getAsLong(ExpressionEntry._ID);
+            long result = mExpressionDB.insert(ExpressionEntry.TABLE_NAME, null, cvs[0]);
+            writeCompleted();
+            // Return 0 on success, row id on failure.
+            if (result == -1) {
+                return index;
+            } else if (result != index) {
+                throw new AssertionError("Expected row id " + index + ", got " + result);
+            } else {
+                return 0L;
+            }
+        }
+        @Override
+        protected void onPostExecute(Long result) {
+            if (result != 0) {
+                synchronized(mLock) {
+                    if (result > 0) {
+                        mMaxAccessible = result - 1;
+                    } else {
+                        mMinAccessible = result + 1;
+                    }
+                }
+                displayDatabaseWarning();
+            }
+        }
+        // On cancellation we do nothing;
+    }
+
+    /**
+     * Add a row with index outside existing range.
+     * The returned index will be just 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.
+     * This ensures that prior additions have completed, but does not wait for this insertion
+     * to complete.
+     */
+    public long addRow(boolean negativeIndex, RowData data) {
+        long result;
+        long newIndex;
+        waitForDBInitialized();
+        synchronized(mLock) {
+            if (negativeIndex) {
+                newIndex = mMinIndex - 1;
+                mMinIndex = newIndex;
+            } else {
+                newIndex = mMaxIndex + 1;
+                mMaxIndex = newIndex;
+            }
+            if (!inAccessibleRange(newIndex)) {
+                // Just drop it, but go ahead and return a new index to use for the cache.
+                // So long as reads of previously written expressions continue to work,
+                // we should be fine. When the application is restarted, history will revert
+                // to just include values between mMinAccessible and mMaxAccessible.
+                return newIndex;
+            }
+            writeStarted();
+            ContentValues cvs = data.toContentValues();
+            cvs.put(ExpressionEntry._ID, newIndex);
+            AsyncWriter awriter = new AsyncWriter();
+            // Ensure that writes are executed in order.
+            awriter.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, cvs);
+        }
+        return newIndex;
+    }
+
+    /**
+     * Generate a fake database row that's good enough to hopefully prevent crashes,
+     * but bad enough to avoid confusion with real data. In particular, the result
+     * will fail to evaluate.
+     */
+    RowData makeBadRow() {
+        CalculatorExpr badExpr = new CalculatorExpr();
+        badExpr.add(R.id.lparen);
+        badExpr.add(R.id.rparen);
+        return new RowData(badExpr.toBytes(), false, false, 0);
+    }
+
+    /**
+     * Retrieve the row with the given index using a direct query.
+     * Such a row must exist.
+     * We assume that the database has been initialized, and the argument has been range checked.
+     */
+    private RowData getRowDirect(long index) {
+        RowData result;
+        String args[] = new String[] { Long.toString(index) };
+        try (Cursor resultC = mExpressionDB.rawQuery(SQL_GET_ROW, args)) {
+            if (!resultC.moveToFirst()) {
+                setBadDB();
+                return makeBadRow();
+            } else {
+                result = new RowData(resultC.getBlob(1), resultC.getInt(2) /* flags */,
+                        resultC.getLong(3) /* timestamp */);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Retrieve the row at the given offset from mAllCursorBase.
+     * Note the argument is NOT an expression index!
+     * We assume that the database has been initialized, and the argument has been range checked.
+     */
+    private RowData getRowFromCursor(int offset) {
+        RowData result;
+        synchronized(mLock) {
+            if (!mAllCursor.moveToPosition(offset)) {
+                Log.e("Calculator", "Failed to move cursor to position " + offset);
+                setBadDB();
+                return makeBadRow();
+            }
+            return new RowData(mAllCursor.getBlob(1), mAllCursor.getInt(2) /* flags */,
+                        mAllCursor.getLong(3) /* timestamp */);
+        }
+    }
+
+    /**
+     * Retrieve the database row at the given index.
+     * We currently assume that we never read data that we added since we initialized the database.
+     * This makes sense, since we cache it anyway. And we should always cache recently added data.
+     */
+    public RowData getRow(long index) {
+        waitForDBInitialized();
+        if (!inAccessibleRange(index)) {
+            // Even if something went wrong opening or writing the database, we should
+            // not see such read requests, unless they correspond to a persistently
+            // saved index, and we can't retrieve that expression.
+            displayDatabaseWarning();
+            return makeBadRow();
+        }
+        int position =  mAllCursorBase - (int)index;
+        // We currently assume that the only gap between expression indices is the one around 0.
+        if (index < 0) {
+            position -= GAP;
+        }
+        if (position < 0) {
+            throw new AssertionError("Database access out of range, index = " + index
+                    + " rel. pos. = " + position);
+        }
+        if (index < 0) {
+            // Avoid using mAllCursor to read data that's far away from the current position,
+            // since we're likely to have to return to the current position.
+            // This is a heuristic; we don't worry about doing the "wrong" thing in the race case.
+            int endPosition;
+            synchronized(mLock) {
+                CursorWindow window = mAllCursor.getWindow();
+                endPosition = window.getStartPosition() + window.getNumRows();
+            }
+            if (position >= endPosition) {
+                return getRowDirect(index);
+            }
+        }
+        // In the positive index case, it's probably OK to cross a cursor boundary, since
+        // we're much more likely to stay in the new window.
+        return getRowFromCursor(position);
+    }
+
+    public long getMinIndex() {
+        waitForDBInitialized();
+        synchronized(mLock) {
+            return mMinIndex;
+        }
+    }
+
+    public long getMaxIndex() {
+        waitForDBInitialized();
+        synchronized(mLock) {
+            return mMaxIndex;
+        }
+    }
+
+    public void close() {
+        mExpressionDBHelper.close();
+    }
+
+}
diff --git a/src/com/android/calculator2/HistoryAdapter.java b/src/com/android/calculator2/HistoryAdapter.java
new file mode 100644
index 0000000..629abe9
--- /dev/null
+++ b/src/com/android/calculator2/HistoryAdapter.java
@@ -0,0 +1,222 @@
+/*
+ * 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.
+ */
+
+package com.android.calculator2;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+
+/**
+ * Adapter for RecyclerView of HistoryItems.
+ */
+public class HistoryAdapter extends RecyclerView.Adapter<HistoryAdapter.ViewHolder> {
+
+    private static final String TAG = "HistoryAdapter";
+
+    private static final int EMPTY_VIEW_TYPE = 0;
+    public static final int HISTORY_VIEW_TYPE = 1;
+
+    private Evaluator mEvaluator;
+
+    private final Calendar mCalendar = Calendar.getInstance();
+
+    private List<HistoryItem> mDataSet;
+
+    private boolean mIsResultLayout;
+    private boolean mIsOneLine;
+    private boolean mIsDisplayEmpty;
+
+    public HistoryAdapter(ArrayList<HistoryItem> dataSet) {
+        mDataSet = dataSet;
+        setHasStableIds(true);
+    }
+
+    @Override
+    public HistoryAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        final View v;
+        if (viewType == HISTORY_VIEW_TYPE) {
+            v = LayoutInflater.from(parent.getContext())
+                    .inflate(R.layout.history_item, parent, false);
+        } else {
+            v = LayoutInflater.from(parent.getContext())
+                    .inflate(R.layout.empty_history_view, parent, false);
+        }
+        return new ViewHolder(v, viewType);
+    }
+
+    @Override
+    public void onBindViewHolder(final HistoryAdapter.ViewHolder holder, int position) {
+        final HistoryItem item = getItem(position);
+
+        if (item.isEmptyView()) {
+            return;
+        }
+
+        holder.mFormula.setText(item.getFormula());
+        // Note: HistoryItems that are not the current expression will always have interesting ops.
+        holder.mResult.setEvaluator(mEvaluator, item.getEvaluatorIndex());
+        if (item.getEvaluatorIndex() == Evaluator.HISTORY_MAIN_INDEX) {
+            holder.mDate.setText(R.string.title_current_expression);
+            holder.mResult.setVisibility(mIsOneLine ? View.GONE : View.VISIBLE);
+        } else {
+            // If the previous item occurred on the same date, the current item does not need
+            // a date header.
+            if (shouldShowHeader(position, item)) {
+                holder.mDate.setText(item.getDateString());
+                // Special case -- very first item should not have a divider above it.
+                holder.mDivider.setVisibility(position == getItemCount() - 1
+                        ? View.GONE : View.VISIBLE);
+            } else {
+                holder.mDate.setVisibility(View.GONE);
+                holder.mDivider.setVisibility(View.INVISIBLE);
+            }
+        }
+    }
+
+    @Override
+    public void onViewRecycled(ViewHolder holder) {
+        if (holder.getItemViewType() == EMPTY_VIEW_TYPE) {
+            return;
+        }
+        mEvaluator.cancel(holder.getItemId(), true);
+
+        holder.mDate.setVisibility(View.VISIBLE);
+        holder.mDivider.setVisibility(View.VISIBLE);
+        holder.mDate.setText(null);
+        holder.mFormula.setText(null);
+        holder.mResult.setText(null);
+
+        super.onViewRecycled(holder);
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return getItem(position).getEvaluatorIndex();
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        return getItem(position).isEmptyView() ? EMPTY_VIEW_TYPE : HISTORY_VIEW_TYPE;
+    }
+
+    @Override
+    public int getItemCount() {
+        return mDataSet.size();
+    }
+
+    public void setDataSet(ArrayList<HistoryItem> dataSet) {
+        mDataSet = dataSet;
+    }
+
+    public void setIsResultLayout(boolean isResult) {
+        mIsResultLayout = isResult;
+    }
+
+    public void setIsOneLine(boolean isOneLine) {
+        mIsOneLine = isOneLine;
+    }
+
+    public void setIsDisplayEmpty(boolean isDisplayEmpty) {
+        mIsDisplayEmpty = isDisplayEmpty;
+    }
+
+    public void setEvaluator(Evaluator evaluator) {
+        mEvaluator = evaluator;
+    }
+
+    private int getEvaluatorIndex(int position) {
+        if (mIsDisplayEmpty || mIsResultLayout) {
+            return (int) (mEvaluator.getMaxIndex() - position);
+        } else {
+            // Account for the additional "Current Expression" with the +1.
+            return (int) (mEvaluator.getMaxIndex() - position + 1);
+        }
+    }
+
+    private boolean shouldShowHeader(int position, HistoryItem item) {
+        if (position == getItemCount() - 1) {
+            // First/oldest element should always show the header.
+            return true;
+        }
+        final HistoryItem prevItem = getItem(position + 1);
+        // We need to use Calendars to determine this because of Daylight Savings.
+        mCalendar.setTimeInMillis(item.getTimeInMillis());
+        final int year = mCalendar.get(Calendar.YEAR);
+        final int day = mCalendar.get(Calendar.DAY_OF_YEAR);
+        mCalendar.setTimeInMillis(prevItem.getTimeInMillis());
+        final int prevYear = mCalendar.get(Calendar.YEAR);
+        final int prevDay = mCalendar.get(Calendar.DAY_OF_YEAR);
+        return year != prevYear || day != prevDay;
+    }
+
+    /**
+     * Gets the HistoryItem from mDataSet, lazy-filling the dataSet if necessary.
+     */
+    private HistoryItem getItem(int position) {
+        HistoryItem item = mDataSet.get(position);
+        // Lazy-fill the data set.
+        if (item == null) {
+            final int evaluatorIndex = getEvaluatorIndex(position);
+            item = new HistoryItem(evaluatorIndex,
+                    mEvaluator.getTimeStamp(evaluatorIndex),
+                    mEvaluator.getExprAsSpannable(evaluatorIndex));
+            mDataSet.set(position, item);
+        }
+        return item;
+    }
+
+    public static class ViewHolder extends RecyclerView.ViewHolder {
+
+        private TextView mDate;
+        private AlignedTextView mFormula;
+        private CalculatorResult mResult;
+        private View mDivider;
+
+        public ViewHolder(View v, int viewType) {
+            super(v);
+            if (viewType == EMPTY_VIEW_TYPE) {
+                return;
+            }
+            mDate = (TextView) v.findViewById(R.id.history_date);
+            mFormula = (AlignedTextView) v.findViewById(R.id.history_formula);
+            mResult = (CalculatorResult) v.findViewById(R.id.history_result);
+            mDivider = v.findViewById(R.id.history_divider);
+        }
+
+        public AlignedTextView getFormula() {
+            return mFormula;
+        }
+
+        public CalculatorResult getResult() {
+            return mResult;
+        }
+
+        public TextView getDate() {
+            return mDate;
+        }
+
+        public View getDivider() {
+            return mDivider;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/calculator2/HistoryFragment.java b/src/com/android/calculator2/HistoryFragment.java
new file mode 100644
index 0000000..c37241c
--- /dev/null
+++ b/src/com/android/calculator2/HistoryFragment.java
@@ -0,0 +1,243 @@
+/*
+ * 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.
+ */
+
+package com.android.calculator2;
+
+import android.animation.Animator;
+import android.app.Fragment;
+import android.os.Bundle;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toolbar;
+
+import java.util.ArrayList;
+
+import static android.support.v7.widget.RecyclerView.SCROLL_STATE_DRAGGING;
+
+public class HistoryFragment extends Fragment implements DragLayout.DragCallback {
+
+    public static final String TAG = "HistoryFragment";
+    public static final String CLEAR_DIALOG_TAG = "clear";
+
+    private final DragController mDragController = new DragController();
+
+    private RecyclerView mRecyclerView;
+    private HistoryAdapter mAdapter;
+    private DragLayout mDragLayout;
+
+    private Evaluator mEvaluator;
+
+    private ArrayList<HistoryItem> mDataSet = new ArrayList<>();
+
+    private boolean mIsDisplayEmpty;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mAdapter = new HistoryAdapter(mDataSet);
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        final View view = inflater.inflate(
+                R.layout.fragment_history, container, false /* attachToRoot */);
+
+        mDragLayout = (DragLayout) container.getRootView().findViewById(R.id.drag_layout);
+        mDragLayout.addDragCallback(this);
+
+        mRecyclerView = (RecyclerView) view.findViewById(R.id.history_recycler_view);
+        mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+            @Override
+            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+                if (newState == SCROLL_STATE_DRAGGING) {
+                    stopActionModeOrContextMenu();
+                }
+                super.onScrollStateChanged(recyclerView, newState);
+            }
+        });
+
+        // The size of the RecyclerView is not affected by the adapter's contents.
+        mRecyclerView.setHasFixedSize(true);
+        mRecyclerView.setAdapter(mAdapter);
+
+        final Toolbar toolbar = (Toolbar) view.findViewById(R.id.history_toolbar);
+        toolbar.inflateMenu(R.menu.fragment_history);
+        toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
+            @Override
+            public boolean onMenuItemClick(MenuItem item) {
+                if (item.getItemId() == R.id.menu_clear_history) {
+                    final Calculator calculator = (Calculator) getActivity();
+                    AlertDialogFragment.showMessageDialog(calculator, "" /* title */,
+                            getString(R.string.dialog_clear),
+                            getString(R.string.menu_clear_history),
+                            CLEAR_DIALOG_TAG);
+                    return true;
+                }
+                return onOptionsItemSelected(item);
+            }
+        });
+        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                getActivity().onBackPressed();
+            }
+        });
+        return view;
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+
+        final Calculator activity = (Calculator) getActivity();
+        mEvaluator = Evaluator.getInstance(activity);
+        mAdapter.setEvaluator(mEvaluator);
+
+        final boolean isResultLayout = activity.isResultLayout();
+        final boolean isOneLine = activity.isOneLine();
+
+        // Snapshot display state here. For the rest of the lifecycle of this current
+        // HistoryFragment, this is what we will consider the display state.
+        // In rare cases, the display state can change after our adapter is initialized.
+        final CalculatorExpr mainExpr = mEvaluator.getExpr(Evaluator.MAIN_INDEX);
+        mIsDisplayEmpty = mainExpr == null || mainExpr.isEmpty();
+
+        initializeController(isResultLayout, isOneLine, mIsDisplayEmpty);
+
+        final long maxIndex = mEvaluator.getMaxIndex();
+
+        final ArrayList<HistoryItem> newDataSet = new ArrayList<>();
+
+        if (!mIsDisplayEmpty && !isResultLayout) {
+            // Add the current expression as the first element in the list (the layout is
+            // reversed and we want the current expression to be the last one in the
+            // RecyclerView).
+            // If we are in the result state, the result will animate to the last history
+            // element in the list and there will be no "Current Expression."
+            mEvaluator.copyMainToHistory();
+            newDataSet.add(new HistoryItem(Evaluator.HISTORY_MAIN_INDEX,
+                    System.currentTimeMillis(), mEvaluator.getExprAsSpannable(0)));
+        }
+        for (long i = 0; i < maxIndex; ++i) {
+            newDataSet.add(null);
+        }
+        final boolean isEmpty = newDataSet.isEmpty();
+        mRecyclerView.setBackgroundColor(ContextCompat.getColor(activity,
+                isEmpty ? R.color.empty_history_color : R.color.display_background_color));
+        if (isEmpty) {
+            newDataSet.add(new HistoryItem());
+        }
+        mDataSet = newDataSet;
+        mAdapter.setDataSet(mDataSet);
+        mAdapter.setIsResultLayout(isResultLayout);
+        mAdapter.setIsOneLine(activity.isOneLine());
+        mAdapter.setIsDisplayEmpty(mIsDisplayEmpty);
+        mAdapter.notifyDataSetChanged();
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+
+        final Calculator activity = (Calculator) getActivity();
+        mDragController.initializeAnimation(activity.isResultLayout(), activity.isOneLine(),
+                mIsDisplayEmpty);
+    }
+
+    @Override
+    public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) {
+        return mDragLayout.createAnimator(enter);
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+
+        if (mDragLayout != null) {
+            mDragLayout.removeDragCallback(this);
+        }
+
+        if (mEvaluator != null) {
+            // Note that the view is destroyed when the fragment backstack is popped, so
+            // these are essentially called when the DragLayout is closed.
+            mEvaluator.cancelNonMain();
+        }
+    }
+
+    private void initializeController(boolean isResult, boolean isOneLine, boolean isDisplayEmpty) {
+        mDragController.setDisplayFormula(
+                (CalculatorFormula) getActivity().findViewById(R.id.formula));
+        mDragController.setDisplayResult(
+                (CalculatorResult) getActivity().findViewById(R.id.result));
+        mDragController.setToolbar(getActivity().findViewById(R.id.toolbar));
+        mDragController.setEvaluator(mEvaluator);
+        mDragController.initializeController(isResult, isOneLine, isDisplayEmpty);
+    }
+
+    public boolean stopActionModeOrContextMenu() {
+        if (mRecyclerView == null) {
+            return false;
+        }
+        for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
+            final View view = mRecyclerView.getChildAt(i);
+            final HistoryAdapter.ViewHolder viewHolder =
+                    (HistoryAdapter.ViewHolder) mRecyclerView.getChildViewHolder(view);
+            if (viewHolder != null && viewHolder.getResult() != null
+                    && viewHolder.getResult().stopActionModeOrContextMenu()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /* Begin override DragCallback methods. */
+
+    @Override
+    public void onStartDraggingOpen() {
+        // no-op
+    }
+
+    @Override
+    public void onInstanceStateRestored(boolean isOpen) {
+        if (isOpen) {
+            mRecyclerView.setVisibility(View.VISIBLE);
+        }
+    }
+
+    @Override
+    public void whileDragging(float yFraction) {
+        if (isVisible() || isRemoving()) {
+            mDragController.animateViews(yFraction, mRecyclerView);
+        }
+    }
+
+    @Override
+    public boolean shouldCaptureView(View view, int x, int y) {
+        return !mRecyclerView.canScrollVertically(1 /* scrolling down */);
+    }
+
+    @Override
+    public int getDisplayHeight() {
+        return 0;
+    }
+
+    /* End override DragCallback methods. */
+}
diff --git a/src/com/android/calculator2/HistoryItem.java b/src/com/android/calculator2/HistoryItem.java
new file mode 100644
index 0000000..f20d1a7
--- /dev/null
+++ b/src/com/android/calculator2/HistoryItem.java
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+package com.android.calculator2;
+
+import android.text.Spannable;
+import android.text.format.DateUtils;
+
+public class HistoryItem {
+
+    private long mEvaluatorIndex;
+    /** Date in millis */
+    private long mTimeInMillis;
+    private Spannable mFormula;
+
+    /** This is true only for the "empty history" view. */
+    private final boolean mIsEmpty;
+
+    public HistoryItem(long evaluatorIndex, long millis, Spannable formula) {
+        mEvaluatorIndex = evaluatorIndex;
+        mTimeInMillis = millis;
+        mFormula = formula;
+        mIsEmpty = false;
+    }
+
+    public long getEvaluatorIndex() {
+        return mEvaluatorIndex;
+    }
+
+    public HistoryItem() {
+        mIsEmpty = true;
+    }
+
+    public boolean isEmptyView() {
+        return mIsEmpty;
+    }
+
+    /**
+     * @return String in format "n days ago"
+     * For n > 7, the date is returned.
+     */
+    public CharSequence getDateString() {
+        return DateUtils.getRelativeTimeSpanString(mTimeInMillis, System.currentTimeMillis(),
+                DateUtils.DAY_IN_MILLIS, DateUtils.FORMAT_ABBREV_RELATIVE);
+    }
+
+    public long getTimeInMillis() {
+        return mTimeInMillis;
+    }
+
+    public Spannable getFormula() {
+        return mFormula;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/calculator2/KeyMaps.java b/src/com/android/calculator2/KeyMaps.java
index e82f35d..cdfe4e4 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.
@@ -344,10 +480,10 @@
     private static HashMap<Character, String> sOutputForResultChar;
 
     /**
-     * Locale string corresponding to preceding map and character constants.
+     * Locale corresponding to preceding map and character constants.
      * We recompute the map if this is not the current locale.
      */
-    private static String sLocaleForMaps = "none";
+    private static Locale sLocaleForMaps = null;
 
     /**
      * Activity to use for looking up buttons.
@@ -431,14 +567,14 @@
         sOutputForResultChar.put(c, button.getText().toString());
     }
 
-    // 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.
+    /**
+     * Ensure that the preceding map and character constants correspond to the current locale.
+     * Called only by UI thread.
+     */
     static void validateMaps() {
         Locale locale = Locale.getDefault();
-        String lname = locale.toString();
-        if (lname != sLocaleForMaps) {
-            Log.v ("Calculator", "Setting local to: " + lname);
+        if (!locale.equals(sLocaleForMaps)) {
+            Log.v ("Calculator", "Setting locale to: " + locale.toLanguageTag());
             sKeyValForFun = new HashMap<String, Integer>();
             sKeyValForFun.put("sin", R.id.fun_sin);
             sKeyValForFun.put("cos", R.id.fun_cos);
@@ -495,7 +631,7 @@
                 addButtonToOutputMap((char)('0' + i), keyForDigVal(i));
             }
 
-            sLocaleForMaps = lname;
+            sLocaleForMaps = locale;
 
         }
     }
diff --git a/src/com/android/calculator2/UnifiedReal.java b/src/com/android/calculator2/UnifiedReal.java
index d3bc947..f6cf50b 100644
--- a/src/com/android/calculator2/UnifiedReal.java
+++ b/src/com/android/calculator2/UnifiedReal.java
@@ -366,6 +366,7 @@
      * Returns a truncated representation of the result.
      * If exactlyTruncatable(), we round correctly towards zero. Otherwise the resulting digit
      * string may occasionally be rounded up instead.
+     * Always includes a decimal point in the result.
      * The result includes n digits to the right of the decimal point.
      * @param n result precision, >= 0
      */
@@ -512,6 +513,7 @@
 
     /**
      * Returns true if values are definitely known not to be equal, false in all other cases.
+     * Performs no approximate evaluation.
      */
     public boolean definitelyNotEquals(UnifiedReal u) {
         boolean isNamed = isNamed(mCrFactor);
@@ -539,6 +541,10 @@
         return mRatFactor.signum() == 0;
     }
 
+    /**
+     * Can this number be determined to be definitely nonzero without performing approximate
+     * evaluation?
+     */
     public boolean definitelyNonZero() {
         return isNamed(mCrFactor) && mRatFactor.signum() != 0;
     }
@@ -861,7 +867,27 @@
     private static final BigInteger BIG_TWO = BigInteger.valueOf(2);
 
     /**
+     * Compute an integral power of a constrive real, using the standard recursive algorithm.
+     * exp is known to be positive.
+     */
+    private static CR recursivePow(CR base, BigInteger exp) {
+        if (exp.equals(BigInteger.ONE)) {
+            return base;
+        }
+        if (exp.and(BigInteger.ONE).intValue() == 1) {
+            return base.multiply(recursivePow(base, exp.subtract(BigInteger.ONE)));
+        }
+        CR tmp = recursivePow(base, exp.shiftRight(1));
+        if (Thread.interrupted()) {
+            throw new CR.AbortedException();
+        }
+        return tmp.multiply(tmp);
+    }
+
+    /**
      * Compute an integral power of this.
+     * This recurses roughly as deeply as the number of bits in the exponent, and can, in
+     * ridiculous cases, result in a stack overflow.
      */
     private UnifiedReal pow(BigInteger exp) {
         if (exp.signum() < 0) {
@@ -894,7 +920,17 @@
                 }
             }
         }
-        return new UnifiedReal(crValue().ln().multiply(CR.valueOf(exp)).exp());
+        if (signum(DEFAULT_COMPARE_TOLERANCE) > 0) {
+            // Safe to take the log. This avoids deep recursion for huge exponents, which
+            // may actually make sense here.
+            return new UnifiedReal(crValue().ln().multiply(CR.valueOf(exp)).exp());
+        } else {
+            // Possibly negative base with integer exponent. Use a recursive computation.
+            // (Another possible option would be to use the absolute value of the base, and then
+            // adjust the sign at the end.  But that would have to be done in the CR
+            // implementation.)
+            return new UnifiedReal(recursivePow(crValue(), exp));
+        }
     }
 
     public UnifiedReal pow(UnifiedReal expon) {
@@ -1027,6 +1063,10 @@
         if (definitelyEquals(ZERO)) {
             return ONE;
         }
+        if (definitelyEquals(ONE)) {
+            // Avoid redundant computations, and ensure we recognize all instances as equal.
+            return E;
+        }
         final BoundedRational crExp = getExp(mCrFactor);
         if (crExp != null) {
             if (mRatFactor.signum() < 0) {
diff --git a/tests/Android.mk b/tests/Android.mk
deleted file mode 100644
index 8a84600..0000000
--- a/tests/Android.mk
+++ /dev/null
@@ -1,13 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_MODULE_TAGS := tests
-
-LOCAL_PACKAGE_NAME := ExactCalculatorTests
-LOCAL_INSTRUMENTATION_FOR := ExactCalculator
-
-LOCAL_SDK_VERSION := current
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-include $(BUILD_PACKAGE)
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
deleted file mode 100644
index 491603d..0000000
--- a/tests/AndroidManifest.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2008 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.
--->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.calculator2.tests">
-
-    <uses-sdk android:minSdkVersion="21" />
-
-    <instrumentation
-        android:name="android.test.InstrumentationTestRunner"
-        android:targetPackage="com.android.calculator2"
-        android:label="BoundedRational and Calculator Functional Test" />
-
-    <application>
-        <uses-library android:name="android.test.runner" />
-    </application>
-
-</manifest>
diff --git a/tests/README.txt b/tests/README.txt
deleted file mode 100644
index bfe35ca..0000000
--- a/tests/README.txt
+++ /dev/null
@@ -1,48 +0,0 @@
-Run on Android with
-
-1) Build the tests.
-2) Install the calculator with
-adb install <tree root>/out/target/product/generic/data/app/ExactCalculator/ExactCalculator.apk
-3) adb install <tree root>/out/target/product/generic/data/app/ExactCalculatorTests/ExactCalculatorTests.apk
-4) adb shell am instrument -w com.android.calculator2.tests/android.test.InstrumentationTestRunner
-
-There are three kinds of tests:
-
-1. A superficial test of calculator functionality through the UI.
-This is a resurrected version of a test that appeared in KitKat.
-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
-is used by the calculator mostly to identify exact results, i.e.
-terminating decimal expansions.  But it's also used to optimize CR
-computations, and bugs in BoundedRational could result in incorrect
-outputs.)
-
-3. A quick test of Evaluator.testUnflipZeroes(), which we do not know how to
-test manually.
-
-We currently have no automatic tests for display formatting corner cases.
-The following numbers have exhibited problems in the past and would be good
-to test.  Some of them are difficult to test automatically, because they
-require scrolling to both ends of the result.  For those with finite
-decimal expansions, it also worth confirming that the "display with leading
-digits" display shows an exact value when scrolled all the way to the right.
-
-Some interesting manual test cases:
-
-10^10 + 10^30
-10^30 + 10^-10
--10^30 + 20
-10^30 + 10^-30
--10^30 - 10^10
--1.2x10^-9
--1.2x10^-8
--1.2x10^-10
--10^-12
-1 - 10^-98
-1 - 10^-100
-1 - 10^-300
-1/-56x10^18 (on a Nexus 7 sized portrait display)
--10^-500 (scroll to see the 1, then scroll back & verify minus sign appears)
diff --git a/tests/src/com/android/calculator2/BoundedRationalTest.java b/tests/src/com/android/calculator2/BoundedRationalTest.java
deleted file mode 100644
index a53d6ad..0000000
--- a/tests/src/com/android/calculator2/BoundedRationalTest.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * 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.
- * 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.
- */
-
-// A test for BoundedRationals package.
-
-package com.android.calculator2;
-
-import com.hp.creals.CR;
-import com.hp.creals.UnaryCRFunction;
-
-import junit.framework.AssertionFailedError;
-import junit.framework.TestCase;
-
-import java.math.BigInteger;
-
-public class BoundedRationalTest extends TestCase {
-    private static void check(boolean x, String s) {
-        if (!x) throw new AssertionFailedError(s);
-    }
-    final static int TEST_PREC = -100; // 100 bits to the right of
-                                       // binary point.
-    private static void checkEq(BoundedRational x, CR y, String s) {
-        check(x.crValue().compareTo(y, TEST_PREC) == 0, s);
-    }
-    private static void checkWeakEq(BoundedRational x, CR y, String s) {
-        if (x != null) checkEq(x, y, s);
-    }
-
-    private final static BoundedRational BR_0 = new BoundedRational(0);
-    private final static BoundedRational BR_M1 = new BoundedRational(-1);
-    private final static BoundedRational BR_2 = new BoundedRational(2);
-    private final static BoundedRational BR_M2 = new BoundedRational(-2);
-    private final static BoundedRational BR_15 = new BoundedRational(15);
-    private final static BoundedRational BR_390 = new BoundedRational(390);
-    private final static BoundedRational BR_M390 = new BoundedRational(-390);
-    private final static CR CR_1 = CR.valueOf(1);
-
-    // We assume that x is simple enough that we don't overflow bounds.
-    private static void checkBR(BoundedRational x) {
-        check(x != null, "test data should not be null");
-        CR xAsCR = x.crValue();
-        checkEq(BoundedRational.add(x, BoundedRational.ONE), xAsCR.add(CR_1),
-                "add 1:" + x);
-        checkEq(BoundedRational.subtract(x, BoundedRational.MINUS_THIRTY),
-                xAsCR.subtract(CR.valueOf(-30)), "sub -30:" + x);
-        checkEq(BoundedRational.multiply(x, BR_15),
-                xAsCR.multiply(CR.valueOf(15)), "multiply 15:" + x);
-        checkEq(BoundedRational.divide(x, BR_15),
-                xAsCR.divide(CR.valueOf(15)), "divide 15:" + x);
-        BigInteger big_x = BoundedRational.asBigInteger(x);
-        long long_x = (big_x == null? 0 : big_x.longValue());
-        if (x.compareTo(BoundedRational.THIRTY) <= 0
-                && x.compareTo(BoundedRational.MINUS_THIRTY) >= 0) {
-            checkWeakEq(BoundedRational.pow(BR_15, x),
-                    CR.valueOf(15).ln().multiply(xAsCR).exp(),
-                    "pow(15,x):" + x);
-        }
-        if (x.signum() > 0) {
-            checkWeakEq(BoundedRational.sqrt(x), xAsCR.sqrt(), "sqrt:" + x);
-            checkEq(BoundedRational.pow(x, BR_15),
-                    xAsCR.ln().multiply(CR.valueOf(15)).exp(),
-                    "pow(x,15):" + x);
-        }
-    }
-
-    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)");
-        check(b.toStringTruncated(1).equals("-0.6"), "(4/-6).toStringT(1)");
-        check(BR_15.toStringTruncated(0).equals("15."), "15.toStringT(1)");
-        check(BR_0.toStringTruncated(2).equals("0.00"), "0.toStringT(2)");
-        checkEq(BR_0, CR.valueOf(0), "0");
-        checkEq(BR_390, CR.valueOf(390), "390");
-        checkEq(BR_15, CR.valueOf(15), "15");
-        checkEq(BR_M390, CR.valueOf(-390), "-390");
-        checkEq(BR_M1, CR.valueOf(-1), "-1");
-        checkEq(BR_2, CR.valueOf(2), "2");
-        checkEq(BR_M2, CR.valueOf(-2), "-2");
-        check(BR_0.signum() == 0, "signum(0)");
-        check(BR_M1.signum() == -1, "signum(-1)");
-        check(BR_2.signum() == 1, "signum(2)");
-        check(BoundedRational.asBigInteger(BR_390).intValue() == 390, "390.asBigInteger()");
-        check(BoundedRational.asBigInteger(BoundedRational.HALF) == null, "1/2.asBigInteger()");
-        check(BoundedRational.asBigInteger(BoundedRational.MINUS_HALF) == null,
-                "-1/2.asBigInteger()");
-        check(BoundedRational.asBigInteger(new BoundedRational(15, -5)).intValue() == -3,
-                "-15/5.asBigInteger()");
-        check(BoundedRational.digitsRequired(BoundedRational.ZERO) == 0, "digitsRequired(0)");
-        check(BoundedRational.digitsRequired(BoundedRational.HALF) == 1, "digitsRequired(1/2)");
-        check(BoundedRational.digitsRequired(BoundedRational.MINUS_HALF) == 1,
-                "digitsRequired(-1/2)");
-        check(BoundedRational.digitsRequired(new BoundedRational(1,-2)) == 1,
-                "digitsRequired(1/-2)");
-        // We check values that include all interesting degree values.
-        BoundedRational r = BR_M390;
-        while (!r.equals(BR_390)) {
-            check(r != null, "loop counter overflowed!");
-            checkBR(r);
-            r = BoundedRational.add(r, BR_15);
-        }
-        checkBR(BoundedRational.HALF);
-        checkBR(BoundedRational.MINUS_HALF);
-        checkBR(BoundedRational.ONE);
-        checkBR(BoundedRational.MINUS_ONE);
-        checkBR(new BoundedRational(1000));
-        checkBR(new BoundedRational(100));
-        checkBR(new BoundedRational(4,9));
-        check(BoundedRational.sqrt(new BoundedRational(4,9)) != null,
-              "sqrt(4/9) is null");
-        checkBR(BoundedRational.negate(new BoundedRational(4,9)));
-        checkBR(new BoundedRational(5,9));
-        checkBR(new BoundedRational(5,10));
-        checkBR(new BoundedRational(5,10));
-        checkBR(new BoundedRational(4,13));
-        checkBR(new BoundedRational(36));
-        checkBR(BoundedRational.negate(new BoundedRational(36)));
-        check(BoundedRational.pow(null, BR_15) == null, "pow(null, 15)");
-    }
-
-    public void testBRexceptions() {
-        try {
-            BoundedRational.divide(BR_390, BoundedRational.ZERO);
-            check(false, "390/0");
-        } catch (ArithmeticException ignored) {}
-        try {
-            BoundedRational.sqrt(BR_M1);
-            check(false, "sqrt(-1)");
-        } catch (ArithmeticException ignored) {}
-    }
-
-    public void testBROverflow() {
-        BoundedRational sum = new BoundedRational(0);
-        long i;
-        for (i = 1; i < 4000; ++i) {
-             sum = BoundedRational.add(sum,
-                        BoundedRational.inverse(new BoundedRational(i)));
-             if (sum == null) break;
-        }
-        // With MAX_SIZE = 10000, we seem to overflow at 3488.
-        check(i > 3000, "Harmonic series overflowed at " + i);
-        check(i < 4000, "Harmonic series didn't overflow");
-    }
-}
diff --git a/tests/src/com/android/calculator2/CalculatorHitSomeButtons.java b/tests/src/com/android/calculator2/CalculatorHitSomeButtons.java
deleted file mode 100644
index a075a64..0000000
--- a/tests/src/com/android/calculator2/CalculatorHitSomeButtons.java
+++ /dev/null
@@ -1,179 +0,0 @@
-/**
- * Copyright (c) 2008, Google Inc.
- *
- * 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.
- */
-
-package com.android.calculator2;
-
-import android.app.Activity;
-import android.app.Instrumentation;
-import android.app.Instrumentation.ActivityMonitor;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.test.ActivityInstrumentationTestCase;
-import android.test.suitebuilder.annotation.LargeTest;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.View;
-import android.widget.Button;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-import android.graphics.Rect;
-import android.test.TouchUtils;
-
-import com.android.calculator2.Calculator;
-import com.android.calculator2.R;
-import com.android.calculator2.CalculatorResult;
-
-/**
- * Instrumentation tests for poking some buttons
- *
- */
-
-public class CalculatorHitSomeButtons extends ActivityInstrumentationTestCase <Calculator>{
-    public boolean setup = false;
-    private static final String TAG = "CalculatorTests";
-    Calculator mActivity = null;
-    Instrumentation mInst = null;
-
-    public CalculatorHitSomeButtons() {
-        super("com.android.calculator2", Calculator.class);
-    }
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-
-        mActivity = getActivity();
-        mInst = getInstrumentation();
-    }
-
-    @Override
-    protected void tearDown() throws Exception {
-        super.tearDown();
-    }
-
-
-    @LargeTest
-    public void testPressSomeKeys() {
-        Log.v(TAG, "Pressing some keys!");
-
-        // Make sure that we clear the output
-        press(KeyEvent.KEYCODE_ENTER);
-        press(KeyEvent.KEYCODE_CLEAR);
-
-        // 3 + 4 * 5 => 23
-        press(KeyEvent.KEYCODE_3);
-        press(KeyEvent.KEYCODE_PLUS);
-        press(KeyEvent.KEYCODE_4);
-        press(KeyEvent.KEYCODE_9 | KeyEvent.META_SHIFT_ON);
-        press(KeyEvent.KEYCODE_5);
-        press(KeyEvent.KEYCODE_ENTER);
-
-        checkDisplay("23");
-    }
-
-
-    @LargeTest
-    public void testTapSomeButtons() {
-        // TODO: This probably makes way too many hardcoded assumptions about locale.
-        // The calculator will need a routine to internationalize the output.
-        // We should use that here, too.
-        Log.v(TAG, "Tapping some buttons!");
-
-        // Make sure that we clear the output
-        tap(R.id.eq);
-        tap(R.id.del);
-
-        // 567 / 3 => 189
-        tap(R.id.digit_5);
-        tap(R.id.digit_6);
-        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");
-
-        // make sure we can continue calculations also
-        // 189 - 789 => -600
-        tap(R.id.op_sub);
-        tap(R.id.digit_7);
-        tap(R.id.digit_8);
-        tap(R.id.digit_9);
-        tap(R.id.eq);
-
-        // 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
-    private void press(int keycode) {
-        mInst.sendKeyDownUpSync(keycode);
-    }
-
-    private void tap(int id) {
-        View view = mActivity.findViewById(id);
-        assertNotNull(view);
-        TouchUtils.clickView(this, view);
-    }
-
-    private void checkDisplay(final String s) {
-    /*
-        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());
-                }
-            });
-        } catch (Throwable e) {
-            fail("unexpected exception" + e);
-        }
-    */
-    }
-
-    private String displayVal() {
-        CalculatorResult display = (CalculatorResult) mActivity.findViewById(R.id.result);
-        assertNotNull(display);
-
-        TextView box = (TextView) display;
-        assertNotNull(box);
-
-        return box.getText().toString();
-    }
-}
-
diff --git a/tests/src/com/android/calculator2/EvaluatorTest.java b/tests/src/com/android/calculator2/EvaluatorTest.java
deleted file mode 100644
index 307aef2..0000000
--- a/tests/src/com/android/calculator2/EvaluatorTest.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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.
- * 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.
- */
-
-package com.android.calculator2;
-
-import junit.framework.AssertionFailedError;
-import junit.framework.TestCase;
-
-/**
- * A test for a few static methods in Evaluator.
- * The most interesting one is for unflipZeroes(), which we don't know how to test with
- * real calculator input.
- */
-public class EvaluatorTest extends TestCase {
-    private static void check(boolean x, String s) {
-        if (!x) throw new AssertionFailedError(s);
-    }
-    public void testUnflipZeroes() {
-        check(Evaluator.unflipZeroes("9.99", 2, "9.998", 3).equals("9.998"), "test 1");
-        check(Evaluator.unflipZeroes("9.99", 2, "10.0000", 4).equals("9.9999"), "test 2");
-        check(Evaluator.unflipZeroes("0.99", 2, "1.00000", 5).equals("0.99999"), "test 3");
-        check(Evaluator.unflipZeroes("0.99", 2, "1.00", 2).equals("0.99"), "test 4");
-        check(Evaluator.unflipZeroes("10.00", 2, "9.9999", 4).equals("9.9999"), "test 5");
-        check(Evaluator.unflipZeroes("-10.00", 2, "-9.9999", 4).equals("-9.9999"), "test 6");
-        check(Evaluator.unflipZeroes("-0.99", 2, "-1.00000000000000", 14)
-                .equals("-0.99999999999999"), "test 7");
-        check(Evaluator.unflipZeroes("12349.99", 2, "12350.00000", 5).equals("12349.99999"),
-                "test 8");
-        check(Evaluator.unflipZeroes("123.4999", 4, "123.5000000", 7).equals("123.4999999"),
-                "test 9");
-    }
-
-    public void testGetMsdIndexOf() {
-        check(Evaluator.getMsdIndexOf("-0.0234") == 4, "getMsdIndexOf(-0.0234)");
-        check(Evaluator.getMsdIndexOf("23.45") == 0, "getMsdIndexOf(23.45)");
-        check(Evaluator.getMsdIndexOf("-0.01") == Evaluator.INVALID_MSD, "getMsdIndexOf(-0.01)");
-    }
-
-    public void testExponentEnd() {
-        check(Evaluator.exponentEnd("xE-2%3", 1) == 4, "exponentEnd(xE-2%3)");
-        check(Evaluator.exponentEnd("xE+2%3", 1) == 1, "exponentEnd(xE+2%3)");
-        check(Evaluator.exponentEnd("xe2%3", 1) == 1, "exponentEnd(xe2%3)");
-        check(Evaluator.exponentEnd("xE123%3", 1) == 5, "exponentEnd(xE123%3)");
-        check(Evaluator.exponentEnd("xE123456789%3", 1) == 1, "exponentEnd(xE123456789%3)");
-    }
-}
diff --git a/tests/src/com/android/calculator2/UnifiedRealTest.java b/tests/src/com/android/calculator2/UnifiedRealTest.java
deleted file mode 100644
index 20ac2b1..0000000
--- a/tests/src/com/android/calculator2/UnifiedRealTest.java
+++ /dev/null
@@ -1,264 +0,0 @@
-/*
- * 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.
- */
-
-// A test for UnifiedReal package.
-
-package com.android.calculator2;
-
-import com.hp.creals.CR;
-import com.hp.creals.UnaryCRFunction;
-
-import junit.framework.AssertionFailedError;
-import junit.framework.TestCase;
-
-import java.math.BigInteger;
-
-public class UnifiedRealTest extends TestCase {
-    private static void check(boolean x, String s) {
-        if (!x) throw new AssertionFailedError(s);
-    }
-    final static int TEST_PREC = -100; // 100 bits to the right of
-                                       // binary point.
-    private static void checkEq(UnifiedReal x, CR y, String s) {
-        check(x.crValue().compareTo(y, TEST_PREC) == 0, s);
-    }
-
-    private final static UnaryCRFunction ASIN = UnaryCRFunction.asinFunction;
-    private final static UnaryCRFunction ACOS = UnaryCRFunction.acosFunction;
-    private final static UnaryCRFunction ATAN = UnaryCRFunction.atanFunction;
-    private final static UnaryCRFunction TAN = UnaryCRFunction.tanFunction;
-    private final static CR CR_1 = CR.ONE;
-
-    private final static CR RADIANS_PER_DEGREE = CR.PI.divide(CR.valueOf(180));
-    private final static CR DEGREES_PER_RADIAN = CR.valueOf(180).divide(CR.PI);
-    private final static CR LN10 = CR.valueOf(10).ln();
-
-    private final static UnifiedReal UR_30 = new UnifiedReal(30);
-    private final static UnifiedReal UR_MINUS30 = new UnifiedReal(-30);
-    private final static UnifiedReal UR_15 = new UnifiedReal(15);
-    private final static UnifiedReal UR_MINUS15 = new UnifiedReal(-15);
-
-    private static CR toRadians(CR x) {
-        return x.multiply(RADIANS_PER_DEGREE);
-    }
-
-    private static CR fromRadians(CR x) {
-        return x.multiply(DEGREES_PER_RADIAN);
-    }
-
-    private static UnifiedReal toRadians(UnifiedReal x) {
-        return x.multiply(UnifiedReal.RADIANS_PER_DEGREE);
-    }
-
-    private static UnifiedReal fromRadians(UnifiedReal x) {
-        return x.divide(UnifiedReal.RADIANS_PER_DEGREE);
-    }
-
-    // We assume that x is simple enough that we don't overflow bounds.
-    private static void checkUR(UnifiedReal x) {
-        CR xAsCr = x.crValue();
-        checkEq(x.add(UnifiedReal.ONE), xAsCr.add(CR_1), "add 1:" + x);
-        checkEq(x.subtract(UR_MINUS30), xAsCr.subtract(CR.valueOf(-30)), "sub -30:" + x);
-        checkEq(x.multiply(UR_15), xAsCr.multiply(CR.valueOf(15)), "multiply 15:" + x);
-        checkEq(x.divide(UR_15), xAsCr.divide(CR.valueOf(15)), "divide 15:" + x);
-        checkEq(x.sin(), xAsCr.sin(), "sin:" + x);
-        checkEq(x.cos(), xAsCr.cos(), "cos:" + x);
-        if (x.cos().definitelyNonZero()) {
-            checkEq(x.tan(), TAN.execute(xAsCr), "tan:" + x);
-        }
-        checkEq(toRadians(x).sin(), toRadians(xAsCr).sin(), "degree sin:" + x);
-        checkEq(toRadians(x).cos(), toRadians(xAsCr).cos(), "degree cos:" + x);
-        BigInteger big_x = x.bigIntegerValue();
-        long long_x = (big_x == null? 0 : big_x.longValue());
-        try {
-            checkEq(toRadians(x).tan(), TAN.execute(toRadians(xAsCr)), "degree tan:" + x);
-            check((long_x - 90) % 180 != 0, "missed undefined tan: " + x);
-        } catch (ArithmeticException ignored) {
-            check((long_x - 90) % 180 == 0, "exception on defined tan: " + x + " " + ignored);
-        }
-        if (x.compareTo(UR_30) <= 0 && x.compareTo(UR_MINUS30) >= 0) {
-            checkEq(x.exp(), xAsCr.exp(), "exp:" + x);
-            checkEq(UR_15.pow(x), CR.valueOf(15).ln().multiply(xAsCr).exp(), "pow(15,x):" + x);             }
-        if (x.compareTo(UnifiedReal.ONE) <= 0
-                && x.compareTo(UnifiedReal.ONE.negate()) >= 0) {
-            checkEq(x.asin(), ASIN.execute(xAsCr), "asin:" + x);
-            checkEq(x.acos(), ACOS.execute(xAsCr), "acos:" + x);
-            checkEq(fromRadians(x.asin()), fromRadians(ASIN.execute(xAsCr)), "degree asin:" + x);
-            checkEq(fromRadians(x.acos()), fromRadians(ACOS.execute(xAsCr)), "degree acos:" + x);
-        }
-        checkEq(x.atan(), ATAN.execute(xAsCr), "atan:" + x);
-        if (x.signum() > 0) {
-            checkEq(x.ln(), xAsCr.ln(), "ln:" + x);
-            checkEq(x.sqrt(), xAsCr.sqrt(), "sqrt:" + x);
-            checkEq(x.pow(UR_15), xAsCr.ln().multiply(CR.valueOf(15)).exp(), "pow(x,15):" + x);
-        }
-    }
-
-    public void testUR() {
-        UnifiedReal b = new UnifiedReal(new BoundedRational(4,-6));
-        check(b.toString().equals("4/-6*1.0000000000"), "toString(4/-6)");
-        check(b.toNiceString().equals("-2/3"), "toNiceString(4/-6)");
-        check(b.toStringTruncated(1).equals("-0.6"), "(4/-6).toString(1)");
-        check(UR_15.toStringTruncated(0).equals("15."), "15.toString(1)");
-        check(UnifiedReal.ZERO.toStringTruncated(2).equals("0.00"), "0.toString(2)");
-        checkEq(UnifiedReal.ZERO, CR.valueOf(0), "0");
-        checkEq(new UnifiedReal(390), CR.valueOf(390), "390");
-        checkEq(UR_15, CR.valueOf(15), "15");
-        checkEq(new UnifiedReal(390).negate(), CR.valueOf(-390), "-390");
-        checkEq(UnifiedReal.ONE.negate(), CR.valueOf(-1), "-1");
-        checkEq(new UnifiedReal(2), CR.valueOf(2), "2");
-        checkEq(new UnifiedReal(-2), CR.valueOf(-2), "-2");
-        check(UnifiedReal.ZERO.signum() == 0, "signum(0)");
-        check(UnifiedReal.ZERO.definitelyZero(), "definitelyZero(0)");
-        check(!UnifiedReal.ZERO.definitelyNonZero(), "definitelyNonZero(0)");
-        check(!UnifiedReal.PI.definitelyZero(), "definitelyZero(pi)");
-        check(UnifiedReal.PI.definitelyNonZero(), "definitelyNonZero(pi)");
-        check(UnifiedReal.ONE.negate().signum() == -1, "signum(-1)");
-        check(new UnifiedReal(2).signum() == 1, "signum(2)");
-        check(UnifiedReal.E.signum() == 1, "signum(e)");
-        check(new UnifiedReal(400).bigIntegerValue().intValue() == 400, "400.bigIntegerValue()");
-        check(UnifiedReal.HALF.bigIntegerValue() == null, "1/2.bigIntegerValue()");
-        check(UnifiedReal.HALF.negate().bigIntegerValue() == null, "-1/2.bigIntegerValue()");
-        check(new UnifiedReal(new BoundedRational(15, -5)).bigIntegerValue().intValue() == -3,
-                "-15/5.asBigInteger()");
-        check(UnifiedReal.ZERO.digitsRequired() == 0, "digitsRequired(0)");
-        check(UnifiedReal.HALF.digitsRequired() == 1, "digitsRequired(1)");
-        check(UnifiedReal.HALF.negate().digitsRequired() == 1, "digitsRequired(-1)");
-        check(UnifiedReal.ONE.divide(new UnifiedReal(-2)).digitsRequired() == 1,
-                "digitsRequired(-2)");
-        check(UnifiedReal.ZERO.fact().definitelyEquals(UnifiedReal.ONE), "0!");
-        check(UnifiedReal.ONE.fact().definitelyEquals(UnifiedReal.ONE), "1!");
-        check(UnifiedReal.TWO.fact().definitelyEquals(UnifiedReal.TWO), "2!");
-        check(new UnifiedReal(15).fact().definitelyEquals(new UnifiedReal(1307674368000L)), "15!");
-        check(UnifiedReal.ONE.exactlyDisplayable(), "1 displayable");
-        check(UnifiedReal.PI.exactlyDisplayable(), "PI displayable");
-        check(UnifiedReal.E.exactlyDisplayable(), "E displayable");
-        check(UnifiedReal.E.divide(UnifiedReal.E).exactlyDisplayable(), "E/E displayable");
-        check(!UnifiedReal.E.divide(UnifiedReal.PI).exactlyDisplayable(), "!E/PI displayable");
-        UnifiedReal r = new UnifiedReal(9).multiply(new UnifiedReal(3).sqrt()).ln();
-        checkEq(r, CR.valueOf(9).multiply(CR.valueOf(3).sqrt()).ln(), "ln(9sqrt(3))");
-        check(r.exactlyDisplayable(), "5/2log3");
-        checkEq(r.exp(), CR.valueOf(9).multiply(CR.valueOf(3).sqrt()), "9sqrt(3)");
-        check(r.exp().exactlyDisplayable(), "9sqrt(3)");
-        check(!UnifiedReal.E.divide(UnifiedReal.PI).definitelyEquals(
-                UnifiedReal.E.divide(UnifiedReal.PI)), "E/PI = E/PI not testable");
-        check(new UnifiedReal(32).sqrt().definitelyEquals(
-                (new UnifiedReal(2).sqrt().multiply(new UnifiedReal(4)))), "sqrt(32)");
-        check(new UnifiedReal(32).ln().divide(UnifiedReal.TWO.ln())
-                .definitelyEquals(new UnifiedReal(5)), "ln(32)");
-        check(new UnifiedReal(10).sqrt().multiply(UnifiedReal.TEN.sqrt())
-                .definitelyEquals(UnifiedReal.TEN), "sqrt(10)^2");
-        check(UnifiedReal.ZERO.leadingBinaryZeroes() == Integer.MAX_VALUE, "0.leadingBinaryZeros");
-        check(new UnifiedReal(new BoundedRational(7,1024)).leadingBinaryZeroes() >= 8,
-                "fract.leadingBinaryZeros");
-        UnifiedReal tmp = UnifiedReal.TEN.pow(new UnifiedReal(-1000));
-        int tmp2 = tmp.leadingBinaryZeroes();
-        check(tmp2 >= 3320 && tmp2 < 4000, "leadingBinaryZeroes(10^-1000)");
-        tmp2 = tmp.multiply(UnifiedReal.PI).leadingBinaryZeroes();
-        check(tmp2 >= 3319 && tmp2 < 4000, "leadingBinaryZeroes(pix10^-1000)");
-        // We check values that include all interesting degree values.
-        r = new UnifiedReal(-390);
-        int i = 0;
-        while (!r.definitelyEquals(new UnifiedReal(390))) {
-            check(i++ < 100, "int loop counter arithmetic failed!");
-            if (i > 100) {
-                break;
-            }
-            checkUR(r);
-            r = r.add(new UnifiedReal(15));
-        }
-        r = UnifiedReal.PI.multiply(new UnifiedReal(-3));
-        final UnifiedReal limit = r.negate();
-        final UnifiedReal increment = UnifiedReal.PI.divide(new UnifiedReal(24));
-        i = 0;
-        while (!r.definitelyEquals(limit)) {
-            check(i++ < 200, "transcendental loop counter arithmetic failed!");
-            if (i > 100) {
-                break;
-            }
-            checkUR(r);
-            r = r.add(increment);
-        }
-        checkUR(UnifiedReal.HALF);
-        checkUR(UnifiedReal.MINUS_HALF);
-        checkUR(UnifiedReal.ONE);
-        checkUR(UnifiedReal.MINUS_ONE);
-        checkUR(new UnifiedReal(1000));
-        checkUR(new UnifiedReal(100));
-        checkUR(new UnifiedReal(new BoundedRational(4,9)));
-        check(new UnifiedReal(new BoundedRational(4,9)).sqrt().definitelyEquals(
-                UnifiedReal.TWO.divide(new UnifiedReal(3))), "sqrt(4/9)");
-        checkUR(new UnifiedReal(new BoundedRational(4,9)).negate());
-        checkUR(new UnifiedReal(new BoundedRational(5,9)));
-        checkUR(new UnifiedReal(new BoundedRational(5,10)));
-        checkUR(new UnifiedReal(new BoundedRational(5,10)));
-        checkUR(new UnifiedReal(new BoundedRational(4,13)));
-        checkUR(new UnifiedReal(36));
-        checkUR(new UnifiedReal(36).negate());
-    }
-
-    public void testFunctionsOnSmall() {
-        // This checks some of the special cases we should handle semi-symbolically.
-        UnifiedReal small = new UnifiedReal(2).pow(new UnifiedReal(-1000));
-        UnifiedReal small2 = new UnifiedReal(-1000).exp();
-        for (int i = 0; i <= 10; i++) {
-            UnifiedReal r = new UnifiedReal(i);
-            UnifiedReal sqrt = r.sqrt();
-            if (i > 1 && i != 4 && i != 9) {
-                check(sqrt.definitelyIrrational() && !sqrt.definitelyRational(), "sqrt !rational");
-            } else {
-                check(!sqrt.definitelyIrrational() && sqrt.definitelyRational(), "sqrt rational");
-            }
-            check(sqrt.definitelyAlgebraic() && !sqrt.definitelyTranscendental(), "sqrt algenraic");
-            check(sqrt.multiply(sqrt).definitelyEquals(r), "sqrt " + i);
-            check(!sqrt.multiply(sqrt).definitelyEquals(r.add(small)), "sqrt small " + i);
-            check(!sqrt.multiply(sqrt).definitelyEquals(r.add(small2)), "sqrt small2 " + i);
-            if (i > 0) {
-                UnifiedReal log = r.ln();
-                check(log.exp().definitelyEquals(r), "log " + i);
-                if (i > 1) {
-                    check(log.definitelyTranscendental(), "log transcendental");
-                    check(!log.definitelyAlgebraic(), "log !algebraic");
-                    check(!log.definitelyRational(), "log !rational");
-                    check(log.definitelyIrrational(), "log !rational again");
-                } else {
-                    check(log.definitelyRational(), "log rational");
-                }
-                check(r.pow(r).ln().definitelyEquals(r.multiply(r.ln())), "ln(r^r)");
-            }
-        }
-    }
-
-    public void testURexceptions() {
-        try {
-            UnifiedReal.MINUS_ONE.ln();
-            check(false, "ln(-1)");
-        } catch (ArithmeticException ignored) {}
-        try {
-            UnifiedReal.MINUS_ONE.sqrt();
-            check(false, "sqrt(-1)");
-        } catch (ArithmeticException ignored) {}
-        try {
-            new UnifiedReal(-2).asin();
-            check(false, "asin(-2)");
-        } catch (ArithmeticException ignored) {}
-        try {
-            new UnifiedReal(-2).acos();
-            check(false, "acos(-2)");
-        } catch (ArithmeticException ignored) {}
-    }
-
-}