diff options
author | Justin Klaassen <justinklaassen@google.com> | 2017-02-24 09:45:33 -0800 |
---|---|---|
committer | Justin Klaassen <justinklaassen@google.com> | 2017-02-24 13:54:11 -0800 |
commit | e5459bb4470d984ee3babfcd311b7547b4bea89e (patch) | |
tree | d25e5ec26612152795b508e41888d6e7b45fbc1d | |
parent | dabd82f7376c87fac9e7b0a0c4b5d800846bc27b (diff) | |
parent | 4596a46e3040aefe570cf250d8f3311c90e1dff8 (diff) | |
download | android_packages_apps_ExactCalculator-e5459bb4470d984ee3babfcd311b7547b4bea89e.tar.gz android_packages_apps_ExactCalculator-e5459bb4470d984ee3babfcd311b7547b4bea89e.tar.bz2 android_packages_apps_ExactCalculator-e5459bb4470d984ee3babfcd311b7547b4bea89e.zip |
Merge remote-tracking branch 'goog/ub-calculator-euler'
Bug: 35489652
Test: lunch aosp_bullhead-userdebug && make -j24
Change-Id: Ibe8659f257b1106213df8befe88678d05ae576bf
82 files changed, 5646 insertions, 2212 deletions
@@ -17,19 +17,34 @@ LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) -LOCAL_MODULE_TAGS := optional +LOCAL_RESOURCE_DIR := packages/apps/ExactCalculator/res -LOCAL_OVERRIDES_PACKAGES := Calculator -LOCAL_PACKAGE_NAME := ExactCalculator +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_SDK_VERSION := current -LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_PACKAGE_NAME := ExactCalculator +LOCAL_OVERRIDES_PACKAGES := Calculator -LOCAL_STATIC_JAVA_LIBRARIES := cr android-support-v4 +LOCAL_SRC_FILES := $(call all-java-files-under, src) LOCAL_PROGUARD_FLAG_FILES := proguard.flags +LOCAL_PROGUARD_FLAG_FILES += ../../../frameworks/support/v7/recyclerview/proguard-rules.pro -include $(BUILD_PACKAGE) +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 -include $(call all-makefiles-under,$(LOCAL_PATH)) +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) 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/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 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/copy.xml b/res/menu/menu_result.xml index 5897f88..15e76cd 100644 --- a/res/menu/copy.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_copy" - android:title="@android:string/copy"/> + <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 index 0e698d5..0e698d5 100644 --- a/res/values-w230dp-h220dp/layout.xml +++ b/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.Activity; 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 @@ public class AlertDialogFragment extends DialogFragment implements DialogInterfa 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 @@ public class AlertDialogFragment extends DialogFragment implements DialogInterfa 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 @@ public class AlertDialogFragment extends DialogFragment implements DialogInterfa 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 @@ public class AlertDialogFragment extends DialogFragment implements DialogInterfa 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 @@ public class BoundedRational { 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 @@ public class BoundedRational { } 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 @@ public class BoundedRational { 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 @@ public class BoundedRational { 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 @@ public class BoundedRational { /** * 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 @@ public class BoundedRational { 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 @@ public class BoundedRational { * 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.ObjectAnimator; 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.graphics.Rect; 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.Spanned; 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.HorizontalScrollView; 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.ObjectInput; 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 @@ public class Calculator extends Activity // 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 @@ public class Calculator extends Activity // 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 @@ public class Calculator extends Activity */ 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 @@ public class Calculator extends Activity } }; + 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 @@ public class Calculator extends Activity 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 @@ public class Calculator extends Activity 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 @@ public class Calculator extends Activity } }); + 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 @@ public class Calculator extends Activity 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 @@ public class Calculator extends Activity } 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 @@ public class Calculator extends Activity 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 @@ public class Calculator extends Activity mClearButton.setVisibility(View.GONE); } - if (mOneLine) { + if (mIsOneLine) { if (mCurrentState == CalculatorState.RESULT || mCurrentState == CalculatorState.EVALUATE || mCurrentState == CalculatorState.ANIMATE) { @@ -382,17 +527,43 @@ public class Calculator extends Activity 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 @@ public class Calculator extends Activity * 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 @@ public class Calculator extends Activity } @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 @@ public class Calculator extends Activity // 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 @@ public class Calculator extends Activity // 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 @@ public class Calculator extends Activity 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 @@ public class Calculator extends Activity } /** - * 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 @@ public class Calculator extends Activity mModeToggle.setText(R.string.mode_deg); mModeToggle.setContentDescription(getString(R.string.desc_switch_deg)); } + } + + private void removeHistoryFragment() { + final FragmentManager manager = getFragmentManager(); + if (manager != null && !manager.isDestroyed()) { + manager.popBackStack(HistoryFragment.TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE); + } - // Show the toolbar to highlight the mode change. - showAndMaybeHideToolbar(); + // When HistoryFragment is hidden, the main Calculator is important for accessibility again. + mMainCalculator.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO); } /** @@ -548,10 +745,10 @@ public class Calculator extends Activity */ 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 @@ public class Calculator extends Activity */ 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 @@ public class Calculator extends Activity 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 @@ public class Calculator extends Activity 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 @@ public class Calculator extends Activity } 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 @@ public class Calculator extends Activity } // 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 @@ public class Calculator extends Activity */ 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 @@ public class Calculator extends Activity 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 @@ public class Calculator extends Activity } 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 @@ public class Calculator extends Activity 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 @@ public class Calculator extends Activity @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 @@ public class Calculator extends Activity // 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 @@ public class Calculator extends Activity 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 @@ public class Calculator extends Activity // 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 @@ public class Calculator extends Activity 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 @@ public class Calculator extends Activity @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 @@ public class Calculator extends Activity 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 @@ public class Calculator extends Activity @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 @@ public class Calculator extends Activity } } - 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 @@ public class Calculator extends Activity } else { msg += res.getString(R.string.approximate); } - displayMessage(msg); + displayMessage(getString(R.string.menu_leading), msg); } /** @@ -1105,7 +1451,7 @@ public class Calculator extends Activity } 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 @@ public class Calculator extends Activity 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 class Calculator extends Activity 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 class CalculatorDisplay extends LinearLayout */ 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.SpannableString; 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 @@ import java.util.IdentityHashMap; * 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 @@ class CalculatorExpr { 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 @@ class CalculatorExpr { * 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 @@ class CalculatorExpr { 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 @@ class CalculatorExpr { } } - // 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 @@ class CalculatorExpr { * 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 @@ class CalculatorExpr { } /** + * 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 @@ class CalculatorExpr { /** * 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 @@ class CalculatorExpr { */ 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 @@ class CalculatorExpr { /** * 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 @@ class CalculatorExpr { 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 @@ class CalculatorExpr { 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 @@ class CalculatorExpr { * 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 @@ class CalculatorExpr { * 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 @@ class CalculatorExpr { } /** + * 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 @@ class CalculatorExpr { * * @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 @@ class CalculatorExpr { // 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 index de2a843..2911df8 100644 --- a/src/com/android/calculator2/CalculatorText.java +++ b/src/com/android/calculator2/CalculatorFormula.java @@ -25,7 +25,9 @@ import android.graphics.Rect; 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.view.View; 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 @@ public class CalculatorText extends AlignedTextView implements MenuItem.OnMenuIt 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 @@ public class CalculatorText extends AlignedTextView implements MenuItem.OnMenuIt } @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 @@ public class CalculatorText extends AlignedTextView implements MenuItem.OnMenuIt 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 @@ public class CalculatorText extends AlignedTextView implements MenuItem.OnMenuIt 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 class CalculatorText extends AlignedTextView implements MenuItem.OnMenuIt 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 @@ public class CalculatorText extends AlignedTextView implements MenuItem.OnMenuIt 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 class CalculatorText extends AlignedTextView implements MenuItem.OnMenuIt 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 @@ public class CalculatorText extends AlignedTextView implements MenuItem.OnMenuIt }); } - 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.graphics.Color; 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 @@ public class CalculatorPadViewPager extends ViewPager { @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); - - // 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(); - - // Reset the previously clicked item index. - 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. + 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. + 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; + } + } + + if (action == MotionEvent.ACTION_DOWN) { + mClickedItemIndex = -1; + } + + // 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.ClipboardManager; 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.MenuInflater; 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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu // 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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu 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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu 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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu 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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu 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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu } } }); + + 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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu } 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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu @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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu 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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu } } - /** - * 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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu * 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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu * @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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu * 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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu ++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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu 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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu * 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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu 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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu /** * 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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu 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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu } 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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu @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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu } /** - * 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 class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu 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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu }); } - 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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu (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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu @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 @@ public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenu 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.View; 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 @@ public class CalculatorScrollView extends HorizontalScrollView { 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 @@ public class CalculatorScrollView extends HorizontalScrollView { 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 @@ import java.util.TimeZone; * 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 @@ class Evaluator { // 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"; - private final Handler mTimeoutHandler; // Used to schedule evaluation timeouts. + /** + * 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>(); + } - // The following are valid only if an evaluation completed successfully. - private UnifiedReal mVal; // Value of mExpr as 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; + } - // 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; + private ConcurrentHashMap<Long, ExprInfo> mExprs = new ConcurrentHashMap<Long, ExprInfo>(); - // 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 + // The database holding persistent expressions. + private ExpressionDB mExprDB; - public static final int INVALID_MSD = Integer.MAX_VALUE; + private ExprInfo mMainExpr; // == mExprs.get(MAIN_INDEX) - // 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; + private SharedPreferences mSharedPrefs; - // Currently running expression evaluator, if any. - private AsyncEvaluator mEvaluator; + private final Handler mTimeoutHandler; // Used to schedule evaluation timeouts. - // The one and only un-cancelled and currently running reevaluator. Touched only by UI thread. - private AsyncReevaluator mCurrentReevaluator; + private void setMainExpr(ExprInfo expr) { + mMainExpr = expr; + mExprs.put(MAIN_INDEX, expr); + } - 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 @@ class Evaluator { } 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 @@ class Evaluator { // 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 @@ class Evaluator { * @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 Evaluator { */ 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 @@ class Evaluator { } } 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 @@ class Evaluator { 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 @@ class Evaluator { * 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 @@ class Evaluator { * 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 @@ class Evaluator { // 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 @@ class Evaluator { * 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 @@ class Evaluator { * @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 @@ class Evaluator { * @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 @@ class Evaluator { } /** - * 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 @@ class Evaluator { * 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 @@ class Evaluator { * 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 @@ class Evaluator { 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 @@ class Evaluator { } /** - * 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; + } + 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; } - // In very odd cases, there can be significant latency to evaluate. - // Don't show obsolete result. - mResult.clear(); - evaluateResult(false); + 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); + 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 { - // 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); + 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 - */ - 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(); + * @return true if we cancelled an initial evaluation + */ + private boolean cancel(ExprInfo expr, boolean quiet) { + if (expr.mEvaluator != null) { + if (quiet && (expr.mEvaluator instanceof AsyncEvaluator)) { + ((AsyncEvaluator)(expr.mEvaluator)).suppressCancelMessage(); + } + // 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; } - mEvaluator.cancel(true); - // There seems to be no good way to wait for cancellation - // to complete, and the evaluation continues to look at - // mExpr, which we will again modify. - // Give ourselves a new copy to work on instead. - mExpr = (CalculatorExpr)mExpr.clone(); - // Approximation of constructive reals should be thread-safe, - // so we can let that continue until it notices the cancellation. - mEvaluator = null; - mChangedValue = true; // Didn't do the expected evaluation. - return true; } return false; } /** - * 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 @@ class Evaluator { */ 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 @@ class Evaluator { /** - * 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 @@ class Evaluator { 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 @@ class Evaluator { } } + /** + * 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 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 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; + } + + /** + * 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 the {@link CalculatorExpr} representation of the current result. + * 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 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 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; } /** - * Abbreviate the current expression to a pre-evaluated expression node. + * 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 @@ class Evaluator { 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; + } + } + + /** + * 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(); + } + } + /** - * Abbreviate current expression, and put result in mSaved. + * 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 @@ class Evaluator { } /** - * 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 @@ class Evaluator { 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()); + return mSavedIndex != 0 && uri.equals(uriForSaved()); } - public void appendSaved() { + /** + * 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; - mLongTimeout |= mLongSavedTimeout; - mExpr.append(mSaved); + mMainExpr.mLongTimeout |= ei.mLongTimeout; + CalculatorExpr collapsed = getCollapsedExpr(index); + if (collapsed != null) { + mMainExpr.mExpr.append(getCollapsedExpr(index)); + } } /** - * Add the power of 10 operator to the expression. + * 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 @@ class Evaluator { 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. + */ + 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 CalculatorExpr getExpr() { - return mExpr; + 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 @@ class Evaluator { 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 @@ public class KeyMaps { 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 @@ public class KeyMaps { } /** + * 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 @@ public class KeyMaps { 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 @@ public class KeyMaps { 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 @@ public class KeyMaps { 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 @@ public class UnifiedReal { * 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 @@ public class UnifiedReal { /** * 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 @@ public class UnifiedReal { 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 @@ public class UnifiedReal { 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 @@ public class UnifiedReal { } } } - 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 @@ public class UnifiedReal { 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) {} - } - -} |