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) {}
- }
-
-}