blob: 81ab1f6511c4e65cd7c6576fa7b6fe4a0e48ffc5 [file] [log] [blame]
Justin Klaassen4b3af052014-05-27 17:53:10 -07001/*
Justin Klaassen12da1ad2016-04-04 14:20:37 -07002 * Copyright (C) 2016 The Android Open Source Project
Justin Klaassen4b3af052014-05-27 17:53:10 -07003 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Hans Boehm013969e2015-04-13 20:29:47 -070017// TODO: Copy & more general paste in formula? Note that this requires
18// great care: Currently the text version of a displayed formula
19// is not directly useful for re-evaluating the formula later, since
20// it contains ellipses representing subexpressions evaluated with
21// a different degree mode. Rather than supporting copy from the
22// formula window, we may eventually want to support generation of a
23// more useful text version in a separate window. It's not clear
24// this is worth the added (code and user) complexity.
Hans Boehm84614952014-11-25 18:46:17 -080025
Justin Klaassen4b3af052014-05-27 17:53:10 -070026package com.android.calculator2;
27
28import android.animation.Animator;
Justin Klaassen5f2a3342014-06-11 17:40:22 -070029import android.animation.Animator.AnimatorListener;
Justin Klaassen4b3af052014-05-27 17:53:10 -070030import android.animation.AnimatorListenerAdapter;
31import android.animation.AnimatorSet;
Justin Klaassen4b3af052014-05-27 17:53:10 -070032import android.animation.ObjectAnimator;
Justin Klaassen44595162015-05-28 17:55:20 -070033import android.animation.PropertyValuesHolder;
Justin Klaassen9d33cdc2016-02-21 14:16:14 -080034import android.app.ActionBar;
Justin Klaassen4b3af052014-05-27 17:53:10 -070035import android.app.Activity;
Annie China8b31db2017-02-09 08:11:22 -080036import android.app.Fragment;
Annie Chin06fd3cf2016-11-07 16:04:33 -080037import android.app.FragmentManager;
Justin Klaassen39297782016-12-19 09:11:38 -080038import android.app.FragmentTransaction;
Justin Klaassenfc5ac822015-06-18 13:15:17 -070039import android.content.ClipData;
Hans Boehm5e6a0ca2015-09-22 17:09:01 -070040import android.content.DialogInterface;
Justin Klaassend36d63e2015-05-05 12:59:36 -070041import android.content.Intent;
Hans Boehmbfe8c222015-04-02 16:26:07 -070042import android.content.res.Resources;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070043import android.graphics.Color;
Justin Klaassen8fff1442014-06-19 10:43:29 -070044import android.graphics.Rect;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070045import android.net.Uri;
Justin Klaassen4b3af052014-05-27 17:53:10 -070046import android.os.Bundle;
Aurimas Liutikas8c43f062018-03-28 08:10:28 -070047import androidx.annotation.NonNull;
48import androidx.annotation.StringRes;
49import androidx.core.content.ContextCompat;
50import androidx.viewpager.widget.ViewPager;
Annie Chine918fd22016-03-09 11:07:54 -080051import android.text.Editable;
Hans Boehm8a4f81c2015-07-09 10:41:25 -070052import android.text.SpannableStringBuilder;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070053import android.text.Spanned;
Annie Chinf360ef02016-03-10 13:45:39 -080054import android.text.TextUtils;
Annie Chine918fd22016-03-09 11:07:54 -080055import android.text.TextWatcher;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070056import android.text.style.ForegroundColorSpan;
Annie Chin532b77e2016-12-06 13:30:35 -080057import android.util.Log;
Justin Klaassen44595162015-05-28 17:55:20 -070058import android.util.Property;
Annie Chine918fd22016-03-09 11:07:54 -080059import android.view.ActionMode;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070060import android.view.KeyCharacterMap;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -070061import android.view.KeyEvent;
Hans Boehm84614952014-11-25 18:46:17 -080062import android.view.Menu;
63import android.view.MenuItem;
Annie Chind0f87d22016-10-24 09:04:12 -070064import android.view.MotionEvent;
Justin Klaassen4b3af052014-05-27 17:53:10 -070065import android.view.View;
66import android.view.View.OnLongClickListener;
Justin Klaassen5f2a3342014-06-11 17:40:22 -070067import android.view.ViewAnimationUtils;
Justin Klaassen8fff1442014-06-19 10:43:29 -070068import android.view.ViewGroupOverlay;
Annie Chine918fd22016-03-09 11:07:54 -080069import android.view.ViewTreeObserver;
Justin Klaassen4b3af052014-05-27 17:53:10 -070070import android.view.animation.AccelerateDecelerateInterpolator;
Annie Chine918fd22016-03-09 11:07:54 -080071import android.widget.HorizontalScrollView;
Justin Klaassenfed941a2014-06-09 18:42:40 +010072import android.widget.TextView;
Justin Klaassend48b7562015-04-16 16:51:38 -070073import android.widget.Toolbar;
Justin Klaassenfed941a2014-06-09 18:42:40 +010074
Christine Franks7452d3a2016-10-27 13:41:18 -070075import com.android.calculator2.CalculatorFormula.OnTextSizeChangeListener;
Hans Boehm84614952014-11-25 18:46:17 -080076
77import java.io.ByteArrayInputStream;
Hans Boehm84614952014-11-25 18:46:17 -080078import java.io.ByteArrayOutputStream;
Hans Boehm84614952014-11-25 18:46:17 -080079import java.io.IOException;
Justin Klaassen721ec842015-05-28 14:30:08 -070080import java.io.ObjectInput;
81import java.io.ObjectInputStream;
82import java.io.ObjectOutput;
83import java.io.ObjectOutputStream;
Christine Franksbd90b792016-11-22 10:28:26 -080084import java.text.DecimalFormatSymbols;
Justin Klaassen4b3af052014-05-27 17:53:10 -070085
Christine Franks1d99be12016-11-14 14:00:36 -080086import static com.android.calculator2.CalculatorFormula.OnFormulaContextMenuClickListener;
87
Hans Boehm8f051c32016-10-03 16:53:58 -070088public class Calculator extends Activity
Christine Franks1d99be12016-11-14 14:00:36 -080089 implements OnTextSizeChangeListener, OnLongClickListener,
Annie Chin52eed7a2017-01-04 15:06:05 -080090 AlertDialogFragment.OnClickListener, Evaluator.EvaluationListener /* for main result */,
91 DragLayout.CloseCallback, DragLayout.DragCallback {
Justin Klaassen2be4fdb2014-08-06 19:54:09 -070092
Annie Chin9a211132016-11-30 12:52:06 -080093 private static final String TAG = "Calculator";
Justin Klaassen2be4fdb2014-08-06 19:54:09 -070094 /**
95 * Constant for an invalid resource id.
96 */
97 public static final int INVALID_RES_ID = -1;
Justin Klaassen4b3af052014-05-27 17:53:10 -070098
99 private enum CalculatorState {
Hans Boehm84614952014-11-25 18:46:17 -0800100 INPUT, // Result and formula both visible, no evaluation requested,
101 // Though result may be visible on bottom line.
102 EVALUATE, // Both visible, evaluation requested, evaluation/animation incomplete.
Hans Boehmc1ea0912015-06-19 15:05:07 -0700103 // Not used for instant result evaluation.
Hans Boehm84614952014-11-25 18:46:17 -0800104 INIT, // Very temporary state used as alternative to EVALUATE
105 // during reinitialization. Do not animate on completion.
Hans Boehma5ea8eb2016-12-01 12:33:38 -0800106 INIT_FOR_RESULT, // Identical to INIT, but evaluation is known to terminate
107 // with result, and current expression has been copied to history.
Hans Boehm84614952014-11-25 18:46:17 -0800108 ANIMATE, // Result computed, animation to enlarge result window in progress.
109 RESULT, // Result displayed, formula invisible.
110 // If we are in RESULT state, the formula was evaluated without
111 // error to initial precision.
Hans Boehm8f051c32016-10-03 16:53:58 -0700112 // The current formula is now also the last history entry.
Hans Boehm84614952014-11-25 18:46:17 -0800113 ERROR // Error displayed: Formula visible, result shows error message.
114 // Display similar to INPUT state.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700115 }
Hans Boehm84614952014-11-25 18:46:17 -0800116 // Normal transition sequence is
117 // INPUT -> EVALUATE -> ANIMATE -> RESULT (or ERROR) -> INPUT
118 // A RESULT -> ERROR transition is possible in rare corner cases, in which
119 // a higher precision evaluation exposes an error. This is possible, since we
120 // initially evaluate assuming we were given a well-defined problem. If we
121 // were actually asked to compute sqrt(<extremely tiny negative number>) we produce 0
122 // unless we are asked for enough precision that we can distinguish the argument from zero.
Hans Boehma5ea8eb2016-12-01 12:33:38 -0800123 // ERROR and RESULT are translated to INIT or INIT_FOR_RESULT state if the application
Hans Boehm84614952014-11-25 18:46:17 -0800124 // is restarted in that state. This leads us to recompute and redisplay the result
Hans Boehma5ea8eb2016-12-01 12:33:38 -0800125 // ASAP. We avoid saving the ANIMATE state or activating history in that state.
126 // In INIT_FOR_RESULT, and RESULT state, a copy of the current
127 // expression has been saved in the history db; in the other non-ANIMATE states,
128 // it has not.
Hans Boehm84614952014-11-25 18:46:17 -0800129 // TODO: Possibly save a bit more information, e.g. its initial display string
130 // or most significant digit position, to speed up restart.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700131
Justin Klaassen44595162015-05-28 17:55:20 -0700132 private final Property<TextView, Integer> TEXT_COLOR =
133 new Property<TextView, Integer>(Integer.class, "textColor") {
134 @Override
135 public Integer get(TextView textView) {
136 return textView.getCurrentTextColor();
137 }
138
139 @Override
140 public void set(TextView textView, Integer textColor) {
141 textView.setTextColor(textColor);
142 }
143 };
144
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800145 private static final String NAME = "Calculator";
Hans Boehm84614952014-11-25 18:46:17 -0800146 private static final String KEY_DISPLAY_STATE = NAME + "_display_state";
Hans Boehm760a9dc2015-04-20 10:27:12 -0700147 private static final String KEY_UNPROCESSED_CHARS = NAME + "_unprocessed_chars";
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800148 /**
149 * Associated value is a byte array holding the evaluator state.
150 */
Hans Boehm84614952014-11-25 18:46:17 -0800151 private static final String KEY_EVAL_STATE = NAME + "_eval_state";
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800152 private static final String KEY_INVERSE_MODE = NAME + "_inverse_mode";
Christine Frankseeff27f2016-07-29 12:05:29 -0700153 /**
154 * Associated value is an boolean holding the visibility state of the toolbar.
155 */
156 private static final String KEY_SHOW_TOOLBAR = NAME + "_show_toolbar";
Justin Klaassen741471e2014-06-11 09:43:44 -0700157
Annie Chine918fd22016-03-09 11:07:54 -0800158 private final ViewTreeObserver.OnPreDrawListener mPreDrawListener =
159 new ViewTreeObserver.OnPreDrawListener() {
160 @Override
161 public boolean onPreDraw() {
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700162 mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
Annie Chine918fd22016-03-09 11:07:54 -0800163 final ViewTreeObserver observer = mFormulaContainer.getViewTreeObserver();
164 if (observer.isAlive()) {
165 observer.removeOnPreDrawListener(this);
166 }
167 return false;
168 }
169 };
170
Christine Frankscbc51fa2017-01-04 21:00:36 -0800171 private final Evaluator.Callback mEvaluatorCallback = new Evaluator.Callback() {
172 @Override
173 public void onMemoryStateChanged() {
174 mFormulaText.onMemoryStateChanged();
175 }
176
177 @Override
178 public void showMessageDialog(@StringRes int title, @StringRes int message,
179 @StringRes int positiveButtonLabel, String tag) {
180 AlertDialogFragment.showMessageDialog(Calculator.this, title, message,
181 positiveButtonLabel, tag);
182
183 }
184 };
185
186 private final OnDisplayMemoryOperationsListener mOnDisplayMemoryOperationsListener =
Christine Franks1d99be12016-11-14 14:00:36 -0800187 new OnDisplayMemoryOperationsListener() {
188 @Override
189 public boolean shouldDisplayMemory() {
190 return mEvaluator.getMemoryIndex() != 0;
191 }
192 };
193
Christine Frankscbc51fa2017-01-04 21:00:36 -0800194 private final OnFormulaContextMenuClickListener mOnFormulaContextMenuClickListener =
Christine Franks1d99be12016-11-14 14:00:36 -0800195 new OnFormulaContextMenuClickListener() {
196 @Override
197 public boolean onPaste(ClipData clip) {
198 final ClipData.Item item = clip.getItemCount() == 0 ? null : clip.getItemAt(0);
199 if (item == null) {
200 // nothing to paste, bail early...
201 return false;
202 }
203
204 // Check if the item is a previously copied result, otherwise paste as raw text.
205 final Uri uri = item.getUri();
206 if (uri != null && mEvaluator.isLastSaved(uri)) {
207 clearIfNotInputState();
208 mEvaluator.appendExpr(mEvaluator.getSavedIndex());
209 redisplayAfterFormulaChange();
210 } else {
211 addChars(item.coerceToText(Calculator.this).toString(), false);
212 }
213 return true;
214 }
215
216 @Override
217 public void onMemoryRecall() {
218 clearIfNotInputState();
219 long memoryIndex = mEvaluator.getMemoryIndex();
220 if (memoryIndex != 0) {
221 mEvaluator.appendExpr(mEvaluator.getMemoryIndex());
222 redisplayAfterFormulaChange();
Hans Boehmcc368502016-12-09 10:44:46 -0800223 }
Christine Franks1d99be12016-11-14 14:00:36 -0800224 }
225 };
226
227
Annie Chine918fd22016-03-09 11:07:54 -0800228 private final TextWatcher mFormulaTextWatcher = new TextWatcher() {
229 @Override
230 public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
231 }
232
233 @Override
234 public void onTextChanged(CharSequence charSequence, int start, int count, int after) {
235 }
236
237 @Override
238 public void afterTextChanged(Editable editable) {
239 final ViewTreeObserver observer = mFormulaContainer.getViewTreeObserver();
240 if (observer.isAlive()) {
241 observer.removeOnPreDrawListener(mPreDrawListener);
242 observer.addOnPreDrawListener(mPreDrawListener);
243 }
244 }
245 };
246
Justin Klaassen4b3af052014-05-27 17:53:10 -0700247 private CalculatorState mCurrentState;
Hans Boehm84614952014-11-25 18:46:17 -0800248 private Evaluator mEvaluator;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700249
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800250 private CalculatorDisplay mDisplayView;
Justin Klaassend48b7562015-04-16 16:51:38 -0700251 private TextView mModeView;
Christine Franks7452d3a2016-10-27 13:41:18 -0700252 private CalculatorFormula mFormulaText;
Justin Klaassen44595162015-05-28 17:55:20 -0700253 private CalculatorResult mResultText;
Annie Chine918fd22016-03-09 11:07:54 -0800254 private HorizontalScrollView mFormulaContainer;
Annie Chin09547532016-10-14 10:59:07 -0700255 private DragLayout mDragLayout;
Justin Klaassend48b7562015-04-16 16:51:38 -0700256
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100257 private ViewPager mPadViewPager;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700258 private View mDeleteButton;
259 private View mClearButton;
Justin Klaassend48b7562015-04-16 16:51:38 -0700260 private View mEqualButton;
Annie Chineb36f952016-12-08 17:27:19 -0800261 private View mMainCalculator;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700262
263 private TextView mInverseToggle;
264 private TextView mModeToggle;
265
Justin Klaassen721ec842015-05-28 14:30:08 -0700266 private View[] mInvertibleButtons;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700267 private View[] mInverseButtons;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700268
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700269 private View mCurrentButton;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700270 private Animator mCurrentAnimator;
271
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700272 // Characters that were recently entered at the end of the display that have not yet
273 // been added to the underlying expression.
274 private String mUnprocessedChars = null;
275
276 // Color to highlight unprocessed characters from physical keyboard.
277 // TODO: should probably match this to the error color?
278 private ForegroundColorSpan mUnprocessedColorSpan = new ForegroundColorSpan(Color.RED);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700279
Annie Chin26e159e2016-05-18 15:17:14 -0700280 // Whether the display is one line.
Annie Chin88613232017-01-04 12:21:52 -0800281 private boolean mIsOneLine;
Annie Chin26e159e2016-05-18 15:17:14 -0700282
Hans Boehm31ea2522016-11-23 17:47:02 -0800283 /**
Hans Boehma5ea8eb2016-12-01 12:33:38 -0800284 * Map the old saved state to a new state reflecting requested result reevaluation.
285 */
286 private CalculatorState mapFromSaved(CalculatorState savedState) {
287 switch (savedState) {
288 case RESULT:
289 case INIT_FOR_RESULT:
290 // Evaluation is expected to terminate normally.
291 return CalculatorState.INIT_FOR_RESULT;
292 case ERROR:
293 case INIT:
294 return CalculatorState.INIT;
295 case EVALUATE:
296 case INPUT:
297 return savedState;
298 default: // Includes ANIMATE state.
299 throw new AssertionError("Impossible saved state");
300 }
301 }
302
303 /**
Hans Boehm31ea2522016-11-23 17:47:02 -0800304 * Restore Evaluator state and mCurrentState from savedInstanceState.
305 * Return true if the toolbar should be visible.
306 */
307 private void restoreInstanceState(Bundle savedInstanceState) {
308 final CalculatorState savedState = CalculatorState.values()[
309 savedInstanceState.getInt(KEY_DISPLAY_STATE,
310 CalculatorState.INPUT.ordinal())];
311 setState(savedState);
312 CharSequence unprocessed = savedInstanceState.getCharSequence(KEY_UNPROCESSED_CHARS);
313 if (unprocessed != null) {
314 mUnprocessedChars = unprocessed.toString();
315 }
316 byte[] state = savedInstanceState.getByteArray(KEY_EVAL_STATE);
317 if (state != null) {
318 try (ObjectInput in = new ObjectInputStream(new ByteArrayInputStream(state))) {
319 mEvaluator.restoreInstanceState(in);
320 } catch (Throwable ignored) {
321 // When in doubt, revert to clean state
322 mCurrentState = CalculatorState.INPUT;
323 mEvaluator.clearMain();
324 }
325 }
326 if (savedInstanceState.getBoolean(KEY_SHOW_TOOLBAR, true)) {
327 showAndMaybeHideToolbar();
328 } else {
329 mDisplayView.hideToolbar();
330 }
331 onInverseToggled(savedInstanceState.getBoolean(KEY_INVERSE_MODE));
332 // TODO: We're currently not saving and restoring scroll position.
333 // We probably should. Details may require care to deal with:
334 // - new display size
335 // - slow recomputation if we've scrolled far.
336 }
337
338 private void restoreDisplay() {
339 onModeChanged(mEvaluator.getDegreeMode(Evaluator.MAIN_INDEX));
340 if (mCurrentState != CalculatorState.RESULT
341 && mCurrentState != CalculatorState.INIT_FOR_RESULT) {
342 redisplayFormula();
343 }
344 if (mCurrentState == CalculatorState.INPUT) {
345 // This resultText will explicitly call evaluateAndNotify when ready.
346 mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_EVALUATE, this);
347 } else {
348 // Just reevaluate.
Hans Boehma5ea8eb2016-12-01 12:33:38 -0800349 setState(mapFromSaved(mCurrentState));
Hans Boehm31ea2522016-11-23 17:47:02 -0800350 // Request evaluation when we know display width.
351 mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_REQUIRE, this);
352 }
353 }
354
Justin Klaassen4b3af052014-05-27 17:53:10 -0700355 @Override
356 protected void onCreate(Bundle savedInstanceState) {
357 super.onCreate(savedInstanceState);
Hans Boehm31ea2522016-11-23 17:47:02 -0800358
Annie Chin09547532016-10-14 10:59:07 -0700359 setContentView(R.layout.activity_calculator_main);
Justin Klaassend48b7562015-04-16 16:51:38 -0700360 setActionBar((Toolbar) findViewById(R.id.toolbar));
361
362 // Hide all default options in the ActionBar.
363 getActionBar().setDisplayOptions(0);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700364
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800365 // Ensure the toolbar stays visible while the options menu is displayed.
366 getActionBar().addOnMenuVisibilityListener(new ActionBar.OnMenuVisibilityListener() {
367 @Override
368 public void onMenuVisibilityChanged(boolean isVisible) {
369 mDisplayView.setForceToolbarVisible(isVisible);
370 }
371 });
372
Annie Chineb36f952016-12-08 17:27:19 -0800373 mMainCalculator = findViewById(R.id.main_calculator);
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800374 mDisplayView = (CalculatorDisplay) findViewById(R.id.display);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700375 mModeView = (TextView) findViewById(R.id.mode);
Christine Franks7452d3a2016-10-27 13:41:18 -0700376 mFormulaText = (CalculatorFormula) findViewById(R.id.formula);
Justin Klaassen44595162015-05-28 17:55:20 -0700377 mResultText = (CalculatorResult) findViewById(R.id.result);
Annie Chine918fd22016-03-09 11:07:54 -0800378 mFormulaContainer = (HorizontalScrollView) findViewById(R.id.formula_container);
Hans Boehm31ea2522016-11-23 17:47:02 -0800379 mEvaluator = Evaluator.getInstance(this);
Christine Frankscbc51fa2017-01-04 21:00:36 -0800380 mEvaluator.setCallback(mEvaluatorCallback);
Hans Boehm31ea2522016-11-23 17:47:02 -0800381 mResultText.setEvaluator(mEvaluator, Evaluator.MAIN_INDEX);
382 KeyMaps.setActivity(this);
Justin Klaassend48b7562015-04-16 16:51:38 -0700383
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100384 mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700385 mDeleteButton = findViewById(R.id.del);
386 mClearButton = findViewById(R.id.clr);
Christine Franksbd90b792016-11-22 10:28:26 -0800387 final View numberPad = findViewById(R.id.pad_numeric);
388 mEqualButton = numberPad.findViewById(R.id.eq);
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700389 if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) {
390 mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq);
391 }
Christine Franksbd90b792016-11-22 10:28:26 -0800392 final TextView decimalPointButton = (TextView) numberPad.findViewById(R.id.dec_point);
393 decimalPointButton.setText(getDecimalSeparator());
Justin Klaassene2711cb2015-05-28 11:13:17 -0700394
395 mInverseToggle = (TextView) findViewById(R.id.toggle_inv);
396 mModeToggle = (TextView) findViewById(R.id.toggle_mode);
397
Annie Chin88613232017-01-04 12:21:52 -0800398 mIsOneLine = mResultText.getVisibility() == View.INVISIBLE;
Annie Chin26e159e2016-05-18 15:17:14 -0700399
Justin Klaassen721ec842015-05-28 14:30:08 -0700400 mInvertibleButtons = new View[] {
401 findViewById(R.id.fun_sin),
402 findViewById(R.id.fun_cos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700403 findViewById(R.id.fun_tan),
404 findViewById(R.id.fun_ln),
405 findViewById(R.id.fun_log),
406 findViewById(R.id.op_sqrt)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700407 };
408 mInverseButtons = new View[] {
409 findViewById(R.id.fun_arcsin),
410 findViewById(R.id.fun_arccos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700411 findViewById(R.id.fun_arctan),
412 findViewById(R.id.fun_exp),
413 findViewById(R.id.fun_10pow),
414 findViewById(R.id.op_sqr)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700415 };
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700416
Annie Chin09547532016-10-14 10:59:07 -0700417 mDragLayout = (DragLayout) findViewById(R.id.drag_layout);
Annie Chin52eed7a2017-01-04 15:06:05 -0800418 mDragLayout.removeDragCallback(this);
419 mDragLayout.addDragCallback(this);
420 mDragLayout.setCloseCallback(this);
Annie Chin09547532016-10-14 10:59:07 -0700421
Christine Franks1d99be12016-11-14 14:00:36 -0800422 mFormulaText.setOnContextMenuClickListener(mOnFormulaContextMenuClickListener);
423 mFormulaText.setOnDisplayMemoryOperationsListener(mOnDisplayMemoryOperationsListener);
424
Hans Boehm08e8f322015-04-21 13:18:38 -0700425 mFormulaText.setOnTextSizeChangeListener(this);
Annie Chine918fd22016-03-09 11:07:54 -0800426 mFormulaText.addTextChangedListener(mFormulaTextWatcher);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700427 mDeleteButton.setOnLongClickListener(this);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700428
Hans Boehm31ea2522016-11-23 17:47:02 -0800429 if (savedInstanceState != null) {
430 restoreInstanceState(savedInstanceState);
Christine Frankseeff27f2016-07-29 12:05:29 -0700431 } else {
Hans Boehm31ea2522016-11-23 17:47:02 -0800432 mCurrentState = CalculatorState.INPUT;
433 mEvaluator.clearMain();
Christine Frankseeff27f2016-07-29 12:05:29 -0700434 showAndMaybeHideToolbar();
Hans Boehm31ea2522016-11-23 17:47:02 -0800435 onInverseToggled(false);
Christine Frankseeff27f2016-07-29 12:05:29 -0700436 }
Hans Boehm31ea2522016-11-23 17:47:02 -0800437 restoreDisplay();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700438 }
439
440 @Override
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800441 protected void onResume() {
442 super.onResume();
Christine Frankseeff27f2016-07-29 12:05:29 -0700443 if (mDisplayView.isToolbarVisible()) {
444 showAndMaybeHideToolbar();
445 }
Annie Chineb36f952016-12-08 17:27:19 -0800446 // If HistoryFragment is showing, hide the main Calculator elements from accessibility.
447 // This is because Talkback does not use visibility as a cue for RelativeLayout elements,
448 // and RelativeLayout is the base class of DragLayout.
449 // If we did not do this, it would be possible to traverse to main Calculator elements from
450 // HistoryFragment.
451 mMainCalculator.setImportantForAccessibility(
452 mDragLayout.isOpen() ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
453 : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800454 }
455
456 @Override
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700457 protected void onSaveInstanceState(@NonNull Bundle outState) {
Hans Boehm40125442016-01-22 10:35:35 -0800458 mEvaluator.cancelAll(true);
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700459 // If there's an animation in progress, cancel it first to ensure our state is up-to-date.
460 if (mCurrentAnimator != null) {
461 mCurrentAnimator.cancel();
462 }
463
Justin Klaassen4b3af052014-05-27 17:53:10 -0700464 super.onSaveInstanceState(outState);
Hans Boehm84614952014-11-25 18:46:17 -0800465 outState.putInt(KEY_DISPLAY_STATE, mCurrentState.ordinal());
Hans Boehm760a9dc2015-04-20 10:27:12 -0700466 outState.putCharSequence(KEY_UNPROCESSED_CHARS, mUnprocessedChars);
Hans Boehm84614952014-11-25 18:46:17 -0800467 ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
468 try (ObjectOutput out = new ObjectOutputStream(byteArrayStream)) {
469 mEvaluator.saveInstanceState(out);
470 } catch (IOException e) {
471 // Impossible; No IO involved.
472 throw new AssertionError("Impossible IO exception", e);
473 }
474 outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray());
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800475 outState.putBoolean(KEY_INVERSE_MODE, mInverseToggle.isSelected());
Christine Frankseeff27f2016-07-29 12:05:29 -0700476 outState.putBoolean(KEY_SHOW_TOOLBAR, mDisplayView.isToolbarVisible());
Hans Boehme95203e2017-01-04 14:13:11 -0800477 // We must wait for asynchronous writes to complete, since outState may contain
478 // references to expressions being written.
479 mEvaluator.waitForWrites();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700480 }
481
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700482 // Set the state, updating delete label and display colors.
483 // This restores display positions on moving to INPUT.
Justin Klaassend48b7562015-04-16 16:51:38 -0700484 // But movement/animation for moving to RESULT has already been done.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700485 private void setState(CalculatorState state) {
486 if (mCurrentState != state) {
Hans Boehm84614952014-11-25 18:46:17 -0800487 if (state == CalculatorState.INPUT) {
Hans Boehmd4959e82016-11-15 18:01:28 -0800488 // We'll explicitly request evaluation from now on.
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800489 mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_NOT_EVALUATE, null);
Hans Boehm84614952014-11-25 18:46:17 -0800490 restoreDisplayPositions();
491 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700492 mCurrentState = state;
493
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700494 if (mCurrentState == CalculatorState.RESULT) {
495 // No longer do this for ERROR; allow mistakes to be corrected.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700496 mDeleteButton.setVisibility(View.GONE);
497 mClearButton.setVisibility(View.VISIBLE);
498 } else {
499 mDeleteButton.setVisibility(View.VISIBLE);
500 mClearButton.setVisibility(View.GONE);
501 }
502
Annie Chin88613232017-01-04 12:21:52 -0800503 if (mIsOneLine) {
Annie Chin26e159e2016-05-18 15:17:14 -0700504 if (mCurrentState == CalculatorState.RESULT
505 || mCurrentState == CalculatorState.EVALUATE
506 || mCurrentState == CalculatorState.ANIMATE) {
507 mFormulaText.setVisibility(View.VISIBLE);
508 mResultText.setVisibility(View.VISIBLE);
Annie Chin947d93b2016-06-14 10:18:54 -0700509 } else if (mCurrentState == CalculatorState.ERROR) {
510 mFormulaText.setVisibility(View.INVISIBLE);
511 mResultText.setVisibility(View.VISIBLE);
Annie Chin26e159e2016-05-18 15:17:14 -0700512 } else {
513 mFormulaText.setVisibility(View.VISIBLE);
514 mResultText.setVisibility(View.INVISIBLE);
515 }
516 }
517
Hans Boehm84614952014-11-25 18:46:17 -0800518 if (mCurrentState == CalculatorState.ERROR) {
Chenjie Yu3937b652016-06-01 23:14:26 -0700519 final int errorColor =
520 ContextCompat.getColor(this, R.color.calculator_error_color);
Hans Boehm08e8f322015-04-21 13:18:38 -0700521 mFormulaText.setTextColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700522 mResultText.setTextColor(errorColor);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700523 getWindow().setStatusBarColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700524 } else if (mCurrentState != CalculatorState.RESULT) {
Chenjie Yu3937b652016-06-01 23:14:26 -0700525 mFormulaText.setTextColor(
526 ContextCompat.getColor(this, R.color.display_formula_text_color));
527 mResultText.setTextColor(
528 ContextCompat.getColor(this, R.color.display_result_text_color));
529 getWindow().setStatusBarColor(
Annie Chin96be2462016-12-19 14:31:16 -0800530 ContextCompat.getColor(this, R.color.calculator_statusbar_color));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700531 }
Justin Klaassend48b7562015-04-16 16:51:38 -0700532
533 invalidateOptionsMenu();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700534 }
535 }
536
Annie Chin94c1bd92016-11-23 13:39:56 -0800537 public boolean isResultLayout() {
Hans Boehm31ea2522016-11-23 17:47:02 -0800538 // Note that ERROR has INPUT, not RESULT layout.
539 return mCurrentState == CalculatorState.INIT_FOR_RESULT
540 || mCurrentState == CalculatorState.RESULT;
Annie Chin70ac8ea2016-11-18 14:43:56 -0800541 }
542
Annie Chin88613232017-01-04 12:21:52 -0800543 public boolean isOneLine() {
544 return mIsOneLine;
545 }
546
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700547 @Override
Annie Chind0f87d22016-10-24 09:04:12 -0700548 protected void onDestroy() {
Annie Chin52eed7a2017-01-04 15:06:05 -0800549 mDragLayout.removeDragCallback(this);
Annie Chind0f87d22016-10-24 09:04:12 -0700550 super.onDestroy();
551 }
552
Hans Boehma5ea8eb2016-12-01 12:33:38 -0800553 /**
554 * Destroy the evaluator and close the underlying database.
555 */
556 public void destroyEvaluator() {
557 mEvaluator.destroyEvaluator();
558 }
559
Annie Chind0f87d22016-10-24 09:04:12 -0700560 @Override
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700561 public void onActionModeStarted(ActionMode mode) {
562 super.onActionModeStarted(mode);
Christine Franks7452d3a2016-10-27 13:41:18 -0700563 if (mode.getTag() == CalculatorFormula.TAG_ACTION_MODE) {
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700564 mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
565 }
566 }
567
Chenjie Yu3937b652016-06-01 23:14:26 -0700568 /**
569 * Stop any active ActionMode or ContextMenu for copy/paste actions.
570 * Return true if there was one.
571 */
572 private boolean stopActionModeOrContextMenu() {
Christine Franks7485df52016-12-01 13:18:45 -0800573 return mResultText.stopActionModeOrContextMenu()
574 || mFormulaText.stopActionModeOrContextMenu();
Hans Boehm1176f232015-05-11 16:26:03 -0700575 }
576
Justin Klaassen4b3af052014-05-27 17:53:10 -0700577 @Override
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700578 public void onUserInteraction() {
579 super.onUserInteraction();
580
581 // If there's an animation in progress, end it immediately, so the user interaction can
582 // be handled.
583 if (mCurrentAnimator != null) {
584 mCurrentAnimator.end();
585 }
586 }
587
588 @Override
Christine Franks1473ddd2016-12-01 15:02:23 -0800589 public boolean dispatchTouchEvent(MotionEvent e) {
590 if (e.getActionMasked() == MotionEvent.ACTION_DOWN) {
591 stopActionModeOrContextMenu();
Justin Klaassen39297782016-12-19 09:11:38 -0800592
593 final HistoryFragment historyFragment = getHistoryFragment();
594 if (mDragLayout.isOpen() && historyFragment != null) {
595 historyFragment.stopActionModeOrContextMenu();
Christine Franks1473ddd2016-12-01 15:02:23 -0800596 }
597 }
598 return super.dispatchTouchEvent(e);
599 }
600
601 @Override
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100602 public void onBackPressed() {
Chenjie Yu3937b652016-06-01 23:14:26 -0700603 if (!stopActionModeOrContextMenu()) {
Justin Klaassen39297782016-12-19 09:11:38 -0800604 final HistoryFragment historyFragment = getHistoryFragment();
605 if (mDragLayout.isOpen() && historyFragment != null) {
606 if (!historyFragment.stopActionModeOrContextMenu()) {
Justin Klaassen12874e32016-12-12 07:57:47 -0800607 removeHistoryFragment();
Christine Franks7485df52016-12-01 13:18:45 -0800608 }
Annie Chin09547532016-10-14 10:59:07 -0700609 return;
610 }
Hans Boehm1176f232015-05-11 16:26:03 -0700611 if (mPadViewPager != null && mPadViewPager.getCurrentItem() != 0) {
612 // Select the previous pad.
613 mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
614 } else {
615 // If the user is currently looking at the first pad (or the pad is not paged),
616 // allow the system to handle the Back button.
617 super.onBackPressed();
618 }
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100619 }
620 }
621
622 @Override
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700623 public boolean onKeyUp(int keyCode, KeyEvent event) {
Justin Klaassen83959da2016-04-06 11:55:24 -0700624 // Allow the system to handle special key codes (e.g. "BACK" or "DPAD").
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700625 switch (keyCode) {
Justin Klaassen83959da2016-04-06 11:55:24 -0700626 case KeyEvent.KEYCODE_BACK:
Christine Franksf9ba2202016-10-20 17:20:19 -0700627 case KeyEvent.KEYCODE_ESCAPE:
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700628 case KeyEvent.KEYCODE_DPAD_UP:
629 case KeyEvent.KEYCODE_DPAD_DOWN:
630 case KeyEvent.KEYCODE_DPAD_LEFT:
631 case KeyEvent.KEYCODE_DPAD_RIGHT:
632 return super.onKeyUp(keyCode, event);
633 }
634
Chenjie Yu3937b652016-06-01 23:14:26 -0700635 // Stop the action mode or context menu if it's showing.
636 stopActionModeOrContextMenu();
Justin Klaassend12e0622016-04-27 16:26:47 -0700637
Hans Boehmced295e2016-11-17 17:30:13 -0800638 // Always cancel unrequested in-progress evaluation of the main expression, so that
639 // we don't have to worry about subsequent asynchronous completion.
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700640 // Requested in-progress evaluations are handled below.
Hans Boehm31ea2522016-11-23 17:47:02 -0800641 cancelUnrequested();
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700642
643 switch (keyCode) {
644 case KeyEvent.KEYCODE_NUMPAD_ENTER:
645 case KeyEvent.KEYCODE_ENTER:
646 case KeyEvent.KEYCODE_DPAD_CENTER:
647 mCurrentButton = mEqualButton;
648 onEquals();
649 return true;
650 case KeyEvent.KEYCODE_DEL:
651 mCurrentButton = mDeleteButton;
652 onDelete();
653 return true;
Annie Chin56bcbf12016-09-23 17:04:22 -0700654 case KeyEvent.KEYCODE_CLEAR:
655 mCurrentButton = mClearButton;
656 onClear();
657 return true;
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700658 default:
659 cancelIfEvaluating(false);
660 final int raw = event.getKeyCharacterMap().get(keyCode, event.getMetaState());
661 if ((raw & KeyCharacterMap.COMBINING_ACCENT) != 0) {
662 return true; // discard
663 }
664 // Try to discard non-printing characters and the like.
665 // The user will have to explicitly delete other junk that gets past us.
666 if (Character.isIdentifierIgnorable(raw) || Character.isWhitespace(raw)) {
667 return true;
668 }
669 char c = (char) raw;
670 if (c == '=') {
671 mCurrentButton = mEqualButton;
672 onEquals();
673 } else {
674 addChars(String.valueOf(c), true);
675 redisplayAfterFormulaChange();
676 }
677 return true;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700678 }
679 }
680
Justin Klaassene2711cb2015-05-28 11:13:17 -0700681 /**
682 * Invoked whenever the inverse button is toggled to update the UI.
683 *
684 * @param showInverse {@code true} if inverse functions should be shown
685 */
686 private void onInverseToggled(boolean showInverse) {
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800687 mInverseToggle.setSelected(showInverse);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700688 if (showInverse) {
689 mInverseToggle.setContentDescription(getString(R.string.desc_inv_on));
Justin Klaassen721ec842015-05-28 14:30:08 -0700690 for (View invertibleButton : mInvertibleButtons) {
691 invertibleButton.setVisibility(View.GONE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700692 }
693 for (View inverseButton : mInverseButtons) {
694 inverseButton.setVisibility(View.VISIBLE);
695 }
696 } else {
697 mInverseToggle.setContentDescription(getString(R.string.desc_inv_off));
Justin Klaassen721ec842015-05-28 14:30:08 -0700698 for (View invertibleButton : mInvertibleButtons) {
699 invertibleButton.setVisibility(View.VISIBLE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700700 }
701 for (View inverseButton : mInverseButtons) {
702 inverseButton.setVisibility(View.GONE);
703 }
704 }
705 }
706
707 /**
Christine Frankseeff27f2016-07-29 12:05:29 -0700708 * Invoked whenever the deg/rad mode may have changed to update the UI. Note that the mode has
709 * not necessarily actually changed where this is invoked.
Justin Klaassene2711cb2015-05-28 11:13:17 -0700710 *
711 * @param degreeMode {@code true} if in degree mode
712 */
713 private void onModeChanged(boolean degreeMode) {
714 if (degreeMode) {
Justin Klaassend48b7562015-04-16 16:51:38 -0700715 mModeView.setText(R.string.mode_deg);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700716 mModeView.setContentDescription(getString(R.string.desc_mode_deg));
717
718 mModeToggle.setText(R.string.mode_rad);
719 mModeToggle.setContentDescription(getString(R.string.desc_switch_rad));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700720 } else {
Justin Klaassend48b7562015-04-16 16:51:38 -0700721 mModeView.setText(R.string.mode_rad);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700722 mModeView.setContentDescription(getString(R.string.desc_mode_rad));
723
724 mModeToggle.setText(R.string.mode_deg);
725 mModeToggle.setContentDescription(getString(R.string.desc_switch_deg));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700726 }
727 }
Hans Boehm84614952014-11-25 18:46:17 -0800728
Justin Klaassen12874e32016-12-12 07:57:47 -0800729 private void removeHistoryFragment() {
Annie Chin06fd3cf2016-11-07 16:04:33 -0800730 final FragmentManager manager = getFragmentManager();
Annie Chineb36f952016-12-08 17:27:19 -0800731 if (manager != null && !manager.isDestroyed()) {
Annie Chin2a0f9c02017-01-31 17:00:10 -0800732 manager.popBackStack(HistoryFragment.TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE);
Annie Chin06fd3cf2016-11-07 16:04:33 -0800733 }
Annie Chineb36f952016-12-08 17:27:19 -0800734
735 // When HistoryFragment is hidden, the main Calculator is important for accessibility again.
736 mMainCalculator.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
Annie Chin06fd3cf2016-11-07 16:04:33 -0800737 }
Annie Chin9a211132016-11-30 12:52:06 -0800738
Hans Boehm5d79d102015-09-16 16:33:47 -0700739 /**
740 * Switch to INPUT from RESULT state in response to input of the specified button_id.
741 * View.NO_ID is treated as an incomplete function id.
742 */
743 private void switchToInput(int button_id) {
744 if (KeyMaps.isBinary(button_id) || KeyMaps.isSuffix(button_id)) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700745 mEvaluator.collapse(mEvaluator.getMaxIndex() /* Most recent history entry */);
Hans Boehm5d79d102015-09-16 16:33:47 -0700746 } else {
747 announceClearedForAccessibility();
Hans Boehm8f051c32016-10-03 16:53:58 -0700748 mEvaluator.clearMain();
Hans Boehm5d79d102015-09-16 16:33:47 -0700749 }
750 setState(CalculatorState.INPUT);
751 }
752
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700753 // Add the given button id to input expression.
754 // If appropriate, clear the expression before doing so.
755 private void addKeyToExpr(int id) {
756 if (mCurrentState == CalculatorState.ERROR) {
757 setState(CalculatorState.INPUT);
758 } else if (mCurrentState == CalculatorState.RESULT) {
Hans Boehm5d79d102015-09-16 16:33:47 -0700759 switchToInput(id);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700760 }
761 if (!mEvaluator.append(id)) {
762 // TODO: Some user visible feedback?
763 }
764 }
765
Hans Boehm017de982015-06-10 17:46:03 -0700766 /**
767 * Add the given button id to input expression, assuming it was explicitly
768 * typed/touched.
769 * We perform slightly more aggressive correction than in pasted expressions.
770 */
771 private void addExplicitKeyToExpr(int id) {
772 if (mCurrentState == CalculatorState.INPUT && id == R.id.op_sub) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700773 mEvaluator.getExpr(Evaluator.MAIN_INDEX).removeTrailingAdditiveOperators();
Hans Boehm017de982015-06-10 17:46:03 -0700774 }
775 addKeyToExpr(id);
776 }
777
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800778 public void evaluateInstantIfNecessary() {
779 if (mCurrentState == CalculatorState.INPUT
780 && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
781 mEvaluator.evaluateAndNotify(Evaluator.MAIN_INDEX, this, mResultText);
782 }
783 }
784
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700785 private void redisplayAfterFormulaChange() {
786 // TODO: Could do this more incrementally.
787 redisplayFormula();
788 setState(CalculatorState.INPUT);
Hans Boehm8f051c32016-10-03 16:53:58 -0700789 mResultText.clear();
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800790 if (haveUnprocessed()) {
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800791 // Force reevaluation when text is deleted, even if expression is unchanged.
792 mEvaluator.touch();
793 } else {
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800794 evaluateInstantIfNecessary();
Hans Boehmc023b732015-04-29 11:30:47 -0700795 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700796 }
797
Hans Boehm52d477a2016-04-01 17:42:50 -0700798 /**
799 * Show the toolbar.
800 * Automatically hide it again if it's not relevant to current formula.
801 */
802 private void showAndMaybeHideToolbar() {
803 final boolean shouldBeVisible =
804 mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
805 mDisplayView.showToolbar(!shouldBeVisible);
806 }
807
808 /**
809 * Display or hide the toolbar depending on calculator state.
810 */
811 private void showOrHideToolbar() {
812 final boolean shouldBeVisible =
813 mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
814 if (shouldBeVisible) {
815 mDisplayView.showToolbar(false);
816 } else {
817 mDisplayView.hideToolbar();
818 }
819 }
820
Justin Klaassen4b3af052014-05-27 17:53:10 -0700821 public void onButtonClick(View view) {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700822 // Any animation is ended before we get here.
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700823 mCurrentButton = view;
Chenjie Yu3937b652016-06-01 23:14:26 -0700824 stopActionModeOrContextMenu();
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800825
Hans Boehmc1ea0912015-06-19 15:05:07 -0700826 // See onKey above for the rationale behind some of the behavior below:
Hans Boehm31ea2522016-11-23 17:47:02 -0800827 cancelUnrequested();
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800828
Justin Klaassend48b7562015-04-16 16:51:38 -0700829 final int id = view.getId();
Hans Boehm84614952014-11-25 18:46:17 -0800830 switch (id) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700831 case R.id.eq:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700832 onEquals();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700833 break;
834 case R.id.del:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700835 onDelete();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700836 break;
837 case R.id.clr:
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700838 onClear();
Hans Boehm52d477a2016-04-01 17:42:50 -0700839 return; // Toolbar visibility adjusted at end of animation.
Justin Klaassene2711cb2015-05-28 11:13:17 -0700840 case R.id.toggle_inv:
841 final boolean selected = !mInverseToggle.isSelected();
842 mInverseToggle.setSelected(selected);
843 onInverseToggled(selected);
Hans Boehmc1ea0912015-06-19 15:05:07 -0700844 if (mCurrentState == CalculatorState.RESULT) {
845 mResultText.redisplay(); // In case we cancelled reevaluation.
846 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700847 break;
848 case R.id.toggle_mode:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700849 cancelIfEvaluating(false);
Hans Boehm8f051c32016-10-03 16:53:58 -0700850 final boolean mode = !mEvaluator.getDegreeMode(Evaluator.MAIN_INDEX);
851 if (mCurrentState == CalculatorState.RESULT
852 && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasTrigFuncs()) {
853 // Capture current result evaluated in old mode.
854 mEvaluator.collapse(mEvaluator.getMaxIndex());
Hans Boehmbfe8c222015-04-02 16:26:07 -0700855 redisplayFormula();
856 }
857 // In input mode, we reinterpret already entered trig functions.
858 mEvaluator.setDegreeMode(mode);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700859 onModeChanged(mode);
Christine Frankseeff27f2016-07-29 12:05:29 -0700860 // Show the toolbar to highlight the mode change.
861 showAndMaybeHideToolbar();
Hans Boehmbfe8c222015-04-02 16:26:07 -0700862 setState(CalculatorState.INPUT);
Justin Klaassen44595162015-05-28 17:55:20 -0700863 mResultText.clear();
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800864 if (!haveUnprocessed()) {
865 evaluateInstantIfNecessary();
Hans Boehmc023b732015-04-29 11:30:47 -0700866 }
Christine Frankseeff27f2016-07-29 12:05:29 -0700867 return;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700868 default:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700869 cancelIfEvaluating(false);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800870 if (haveUnprocessed()) {
871 // For consistency, append as uninterpreted characters.
872 // This may actually be useful for a left parenthesis.
873 addChars(KeyMaps.toString(this, id), true);
874 } else {
875 addExplicitKeyToExpr(id);
876 redisplayAfterFormulaChange();
877 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700878 break;
879 }
Hans Boehm52d477a2016-04-01 17:42:50 -0700880 showOrHideToolbar();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700881 }
882
Hans Boehm84614952014-11-25 18:46:17 -0800883 void redisplayFormula() {
Hans Boehm8f051c32016-10-03 16:53:58 -0700884 SpannableStringBuilder formula
885 = mEvaluator.getExpr(Evaluator.MAIN_INDEX).toSpannableStringBuilder(this);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700886 if (mUnprocessedChars != null) {
887 // Add and highlight characters we couldn't process.
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700888 formula.append(mUnprocessedChars, mUnprocessedColorSpan,
889 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700890 }
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700891 mFormulaText.changeTextTo(formula);
Annie Chinf360ef02016-03-10 13:45:39 -0800892 mFormulaText.setContentDescription(TextUtils.isEmpty(formula)
Justin Klaassend1831412016-07-19 21:59:10 -0700893 ? getString(R.string.desc_formula) : null);
Hans Boehm84614952014-11-25 18:46:17 -0800894 }
895
Justin Klaassen4b3af052014-05-27 17:53:10 -0700896 @Override
897 public boolean onLongClick(View view) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700898 mCurrentButton = view;
899
Justin Klaassen4b3af052014-05-27 17:53:10 -0700900 if (view.getId() == R.id.del) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700901 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700902 return true;
903 }
904 return false;
905 }
906
Hans Boehm84614952014-11-25 18:46:17 -0800907 // Initial evaluation completed successfully. Initiate display.
Hans Boehm8f051c32016-10-03 16:53:58 -0700908 public void onEvaluate(long index, int initDisplayPrec, int msd, int leastDigPos,
Hans Boehma0e45f32015-05-30 13:20:35 -0700909 String truncatedWholeNumber) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700910 if (index != Evaluator.MAIN_INDEX) {
911 throw new AssertionError("Unexpected evaluation result index\n");
912 }
Annie Chin37c33b62016-11-22 14:46:28 -0800913
Justin Klaassend48b7562015-04-16 16:51:38 -0700914 // Invalidate any options that may depend on the current result.
915 invalidateOptionsMenu();
916
Hans Boehm8f051c32016-10-03 16:53:58 -0700917 mResultText.onEvaluate(index, initDisplayPrec, msd, leastDigPos, truncatedWholeNumber);
Hans Boehm31ea2522016-11-23 17:47:02 -0800918 if (mCurrentState != CalculatorState.INPUT) {
Annie China3a33752017-02-09 08:53:18 -0800919 // In EVALUATE, INIT, RESULT, or INIT_FOR_RESULT state.
Hans Boehm45223152016-12-21 10:35:35 -0800920 onResult(mCurrentState == CalculatorState.EVALUATE /* animate */,
Annie China3a33752017-02-09 08:53:18 -0800921 mCurrentState == CalculatorState.INIT_FOR_RESULT
922 || mCurrentState == CalculatorState.RESULT /* previously preserved */);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700923 }
Hans Boehm84614952014-11-25 18:46:17 -0800924 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700925
Hans Boehmc1ea0912015-06-19 15:05:07 -0700926 // Reset state to reflect evaluator cancellation. Invoked by evaluator.
Hans Boehm8f051c32016-10-03 16:53:58 -0700927 public void onCancelled(long index) {
928 // Index is Evaluator.MAIN_INDEX. We should be in EVALUATE state.
Hans Boehm84614952014-11-25 18:46:17 -0800929 setState(CalculatorState.INPUT);
Hans Boehm8f051c32016-10-03 16:53:58 -0700930 mResultText.onCancelled(index);
Hans Boehm84614952014-11-25 18:46:17 -0800931 }
932
933 // Reevaluation completed; ask result to redisplay current value.
Christine Frankscbc51fa2017-01-04 21:00:36 -0800934 public void onReevaluate(long index) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700935 // Index is Evaluator.MAIN_INDEX.
936 mResultText.onReevaluate(index);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700937 }
938
Justin Klaassenfed941a2014-06-09 18:42:40 +0100939 @Override
940 public void onTextSizeChanged(final TextView textView, float oldSize) {
941 if (mCurrentState != CalculatorState.INPUT) {
942 // Only animate text changes that occur from user input.
943 return;
944 }
945
946 // Calculate the values needed to perform the scale and translation animations,
947 // maintaining the same apparent baseline for the displayed text.
948 final float textScale = oldSize / textView.getTextSize();
949 final float translationX = (1.0f - textScale) *
950 (textView.getWidth() / 2.0f - textView.getPaddingEnd());
951 final float translationY = (1.0f - textScale) *
952 (textView.getHeight() / 2.0f - textView.getPaddingBottom());
953
954 final AnimatorSet animatorSet = new AnimatorSet();
955 animatorSet.playTogether(
956 ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f),
957 ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f),
958 ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f),
959 ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f));
Justin Klaassen94db7202014-06-11 11:22:31 -0700960 animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassenfed941a2014-06-09 18:42:40 +0100961 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
962 animatorSet.start();
963 }
964
Hans Boehmc1ea0912015-06-19 15:05:07 -0700965 /**
966 * Cancel any in-progress explicitly requested evaluations.
967 * @param quiet suppress pop-up message. Explicit evaluation can change the expression
968 value, and certainly changes the display, so it seems reasonable to warn.
969 * @return true if there was such an evaluation
970 */
971 private boolean cancelIfEvaluating(boolean quiet) {
972 if (mCurrentState == CalculatorState.EVALUATE) {
Hans Boehm31ea2522016-11-23 17:47:02 -0800973 mEvaluator.cancel(Evaluator.MAIN_INDEX, quiet);
Hans Boehmc1ea0912015-06-19 15:05:07 -0700974 return true;
975 } else {
976 return false;
977 }
978 }
979
Hans Boehm31ea2522016-11-23 17:47:02 -0800980
981 private void cancelUnrequested() {
982 if (mCurrentState == CalculatorState.INPUT) {
983 mEvaluator.cancel(Evaluator.MAIN_INDEX, true);
984 }
985 }
986
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800987 private boolean haveUnprocessed() {
988 return mUnprocessedChars != null && !mUnprocessedChars.isEmpty();
989 }
990
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700991 private void onEquals() {
Hans Boehm56d6e762016-06-06 11:46:29 -0700992 // Ignore if in non-INPUT state, or if there are no operators.
Justin Klaassena8075af2016-07-27 15:24:45 -0700993 if (mCurrentState == CalculatorState.INPUT) {
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800994 if (haveUnprocessed()) {
Justin Klaassena8075af2016-07-27 15:24:45 -0700995 setState(CalculatorState.EVALUATE);
Hans Boehm8f051c32016-10-03 16:53:58 -0700996 onError(Evaluator.MAIN_INDEX, R.string.error_syntax);
997 } else if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
Justin Klaassena8075af2016-07-27 15:24:45 -0700998 setState(CalculatorState.EVALUATE);
Hans Boehm8f051c32016-10-03 16:53:58 -0700999 mEvaluator.requireResult(Evaluator.MAIN_INDEX, this, mResultText);
Hans Boehmaf04c3a2016-01-27 14:50:08 -08001000 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -07001001 }
1002 }
1003
1004 private void onDelete() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001005 // Delete works like backspace; remove the last character or operator from the expression.
1006 // Note that we handle keyboard delete exactly like the delete button. For
1007 // example the delete button can be used to delete a character from an incomplete
1008 // function name typed on a physical keyboard.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001009 // This should be impossible in RESULT state.
Hans Boehmc1ea0912015-06-19 15:05:07 -07001010 // If there is an in-progress explicit evaluation, just cancel it and return.
1011 if (cancelIfEvaluating(false)) return;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001012 setState(CalculatorState.INPUT);
Hans Boehmaf04c3a2016-01-27 14:50:08 -08001013 if (haveUnprocessed()) {
1014 mUnprocessedChars = mUnprocessedChars.substring(0, mUnprocessedChars.length() - 1);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001015 } else {
Hans Boehmc023b732015-04-29 11:30:47 -07001016 mEvaluator.delete();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001017 }
Hans Boehm8f051c32016-10-03 16:53:58 -07001018 if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).isEmpty() && !haveUnprocessed()) {
Hans Boehmdb6f9992015-08-19 12:32:56 -07001019 // Resulting formula won't be announced, since it's empty.
1020 announceClearedForAccessibility();
1021 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001022 redisplayAfterFormulaChange();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -07001023 }
1024
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001025 private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
Justin Klaassen06360f92014-08-28 11:08:44 -07001026 final ViewGroupOverlay groupOverlay =
1027 (ViewGroupOverlay) getWindow().getDecorView().getOverlay();
Justin Klaassen8fff1442014-06-19 10:43:29 -07001028
1029 final Rect displayRect = new Rect();
Justin Klaassen06360f92014-08-28 11:08:44 -07001030 mDisplayView.getGlobalVisibleRect(displayRect);
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001031
1032 // Make reveal cover the display and status bar.
1033 final View revealView = new View(this);
Justin Klaassen8fff1442014-06-19 10:43:29 -07001034 revealView.setBottom(displayRect.bottom);
1035 revealView.setLeft(displayRect.left);
1036 revealView.setRight(displayRect.right);
Chenjie Yu3937b652016-06-01 23:14:26 -07001037 revealView.setBackgroundColor(ContextCompat.getColor(this, colorRes));
Justin Klaassen06360f92014-08-28 11:08:44 -07001038 groupOverlay.add(revealView);
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001039
Justin Klaassen4b3af052014-05-27 17:53:10 -07001040 final int[] clearLocation = new int[2];
1041 sourceView.getLocationInWindow(clearLocation);
1042 clearLocation[0] += sourceView.getWidth() / 2;
1043 clearLocation[1] += sourceView.getHeight() / 2;
1044
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001045 final int revealCenterX = clearLocation[0] - revealView.getLeft();
1046 final int revealCenterY = clearLocation[1] - revealView.getTop();
Justin Klaassen4b3af052014-05-27 17:53:10 -07001047
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001048 final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2);
1049 final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2);
1050 final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2);
Justin Klaassen4b3af052014-05-27 17:53:10 -07001051 final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2));
1052
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001053 final Animator revealAnimator =
1054 ViewAnimationUtils.createCircularReveal(revealView,
ztenghui3d6ecaf2014-06-05 09:56:00 -07001055 revealCenterX, revealCenterY, 0.0f, revealRadius);
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001056 revealAnimator.setDuration(
Justin Klaassen4b3af052014-05-27 17:53:10 -07001057 getResources().getInteger(android.R.integer.config_longAnimTime));
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001058 revealAnimator.addListener(listener);
Justin Klaassen4b3af052014-05-27 17:53:10 -07001059
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001060 final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
Justin Klaassen4b3af052014-05-27 17:53:10 -07001061 alphaAnimator.setDuration(
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001062 getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassen4b3af052014-05-27 17:53:10 -07001063
1064 final AnimatorSet animatorSet = new AnimatorSet();
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001065 animatorSet.play(revealAnimator).before(alphaAnimator);
Justin Klaassen4b3af052014-05-27 17:53:10 -07001066 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
1067 animatorSet.addListener(new AnimatorListenerAdapter() {
1068 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -07001069 public void onAnimationEnd(Animator animator) {
Justin Klaassen8fff1442014-06-19 10:43:29 -07001070 groupOverlay.remove(revealView);
Justin Klaassen4b3af052014-05-27 17:53:10 -07001071 mCurrentAnimator = null;
1072 }
1073 });
1074
1075 mCurrentAnimator = animatorSet;
1076 animatorSet.start();
1077 }
1078
Hans Boehmdb6f9992015-08-19 12:32:56 -07001079 private void announceClearedForAccessibility() {
1080 mResultText.announceForAccessibility(getResources().getString(R.string.cleared));
Hans Boehmccc55662015-07-07 14:16:59 -07001081 }
1082
Hans Boehm9db3ee22016-11-18 10:09:47 -08001083 public void onClearAnimationEnd() {
1084 mUnprocessedChars = null;
1085 mResultText.clear();
1086 mEvaluator.clearMain();
1087 setState(CalculatorState.INPUT);
1088 redisplayFormula();
1089 }
1090
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001091 private void onClear() {
Hans Boehm8f051c32016-10-03 16:53:58 -07001092 if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).isEmpty() && !haveUnprocessed()) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001093 return;
1094 }
Hans Boehmc1ea0912015-06-19 15:05:07 -07001095 cancelIfEvaluating(true);
Hans Boehmdb6f9992015-08-19 12:32:56 -07001096 announceClearedForAccessibility();
Annie Chin96be2462016-12-19 14:31:16 -08001097 reveal(mCurrentButton, R.color.calculator_primary_color, new AnimatorListenerAdapter() {
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001098 @Override
1099 public void onAnimationEnd(Animator animation) {
Hans Boehm9db3ee22016-11-18 10:09:47 -08001100 onClearAnimationEnd();
Hans Boehm52d477a2016-04-01 17:42:50 -07001101 showOrHideToolbar();
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001102 }
1103 });
1104 }
1105
Hans Boehm84614952014-11-25 18:46:17 -08001106 // Evaluation encountered en error. Display the error.
Hans Boehm8f051c32016-10-03 16:53:58 -07001107 @Override
1108 public void onError(final long index, final int errorResourceId) {
1109 if (index != Evaluator.MAIN_INDEX) {
1110 throw new AssertionError("Unexpected error source");
1111 }
Hans Boehmfbcef702015-04-27 18:07:47 -07001112 if (mCurrentState == CalculatorState.EVALUATE) {
1113 setState(CalculatorState.ANIMATE);
Hans Boehmccc55662015-07-07 14:16:59 -07001114 mResultText.announceForAccessibility(getResources().getString(errorResourceId));
Hans Boehmfbcef702015-04-27 18:07:47 -07001115 reveal(mCurrentButton, R.color.calculator_error_color,
1116 new AnimatorListenerAdapter() {
1117 @Override
1118 public void onAnimationEnd(Animator animation) {
1119 setState(CalculatorState.ERROR);
Hans Boehm8f051c32016-10-03 16:53:58 -07001120 mResultText.onError(index, errorResourceId);
Hans Boehmfbcef702015-04-27 18:07:47 -07001121 }
1122 });
Hans Boehm31ea2522016-11-23 17:47:02 -08001123 } else if (mCurrentState == CalculatorState.INIT
1124 || mCurrentState == CalculatorState.INIT_FOR_RESULT /* very unlikely */) {
Hans Boehmfbcef702015-04-27 18:07:47 -07001125 setState(CalculatorState.ERROR);
Hans Boehm8f051c32016-10-03 16:53:58 -07001126 mResultText.onError(index, errorResourceId);
Hans Boehmc023b732015-04-29 11:30:47 -07001127 } else {
Justin Klaassen44595162015-05-28 17:55:20 -07001128 mResultText.clear();
Justin Klaassen2be4fdb2014-08-06 19:54:09 -07001129 }
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001130 }
1131
Hans Boehm84614952014-11-25 18:46:17 -08001132 // Animate movement of result into the top formula slot.
1133 // Result window now remains translated in the top slot while the result is displayed.
1134 // (We convert it back to formula use only when the user provides new input.)
Justin Klaassen44595162015-05-28 17:55:20 -07001135 // Historical note: In the Lollipop version, this invisibly and instantaneously moved
Hans Boehm84614952014-11-25 18:46:17 -08001136 // formula and result displays back at the end of the animation. We no longer do that,
1137 // so that we can continue to properly support scrolling of the result.
1138 // We assume the result already contains the text to be expanded.
Hans Boehm45223152016-12-21 10:35:35 -08001139 private void onResult(boolean animate, boolean resultWasPreserved) {
Justin Klaassen44595162015-05-28 17:55:20 -07001140 // Calculate the textSize that would be used to display the result in the formula.
1141 // For scrollable results just use the minimum textSize to maximize the number of digits
1142 // that are visible on screen.
1143 float textSize = mFormulaText.getMinimumTextSize();
1144 if (!mResultText.isScrollable()) {
1145 textSize = mFormulaText.getVariableTextSize(mResultText.getText().toString());
1146 }
1147
1148 // Scale the result to match the calculated textSize, minimizing the jump-cut transition
1149 // when a result is reused in a subsequent expression.
1150 final float resultScale = textSize / mResultText.getTextSize();
1151
1152 // Set the result's pivot to match its gravity.
1153 mResultText.setPivotX(mResultText.getWidth() - mResultText.getPaddingRight());
1154 mResultText.setPivotY(mResultText.getHeight() - mResultText.getPaddingBottom());
1155
1156 // Calculate the necessary translations so the result takes the place of the formula and
1157 // the formula moves off the top of the screen.
Annie Chin28589dc2016-06-09 17:50:51 -07001158 final float resultTranslationY = (mFormulaContainer.getBottom() - mResultText.getBottom())
1159 - (mFormulaText.getPaddingBottom() - mResultText.getPaddingBottom());
1160 float formulaTranslationY = -mFormulaContainer.getBottom();
Annie Chin88613232017-01-04 12:21:52 -08001161 if (mIsOneLine) {
Annie Chin26e159e2016-05-18 15:17:14 -07001162 // Position the result text.
1163 mResultText.setY(mResultText.getBottom());
Annie Chin28589dc2016-06-09 17:50:51 -07001164 formulaTranslationY = -(findViewById(R.id.toolbar).getBottom()
1165 + mFormulaContainer.getBottom());
Annie Chin26e159e2016-05-18 15:17:14 -07001166 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001167
Justin Klaassen44595162015-05-28 17:55:20 -07001168 // Change the result's textColor to match the formula.
1169 final int formulaTextColor = mFormulaText.getCurrentTextColor();
1170
Hans Boehm45223152016-12-21 10:35:35 -08001171 if (resultWasPreserved) {
1172 // Result was previously addded to history.
1173 mEvaluator.represerve();
1174 } else {
Hans Boehma5ea8eb2016-12-01 12:33:38 -08001175 // Add current result to history.
Hans Boehm8bf0dca2017-01-25 17:10:39 -08001176 mEvaluator.preserve(Evaluator.MAIN_INDEX, true);
Hans Boehm45223152016-12-21 10:35:35 -08001177 }
Hans Boehma5ea8eb2016-12-01 12:33:38 -08001178
Hans Boehm45223152016-12-21 10:35:35 -08001179 if (animate) {
Hans Boehmccc55662015-07-07 14:16:59 -07001180 mResultText.announceForAccessibility(getResources().getString(R.string.desc_eq));
1181 mResultText.announceForAccessibility(mResultText.getText());
Hans Boehmc1ea0912015-06-19 15:05:07 -07001182 setState(CalculatorState.ANIMATE);
Hans Boehm84614952014-11-25 18:46:17 -08001183 final AnimatorSet animatorSet = new AnimatorSet();
1184 animatorSet.playTogether(
Justin Klaassen44595162015-05-28 17:55:20 -07001185 ObjectAnimator.ofPropertyValuesHolder(mResultText,
1186 PropertyValuesHolder.ofFloat(View.SCALE_X, resultScale),
1187 PropertyValuesHolder.ofFloat(View.SCALE_Y, resultScale),
1188 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, resultTranslationY)),
1189 ObjectAnimator.ofArgb(mResultText, TEXT_COLOR, formulaTextColor),
Annie Chine918fd22016-03-09 11:07:54 -08001190 ObjectAnimator.ofFloat(mFormulaContainer, View.TRANSLATION_Y,
1191 formulaTranslationY));
Justin Klaassen44595162015-05-28 17:55:20 -07001192 animatorSet.setDuration(getResources().getInteger(
1193 android.R.integer.config_longAnimTime));
Hans Boehm84614952014-11-25 18:46:17 -08001194 animatorSet.addListener(new AnimatorListenerAdapter() {
1195 @Override
Hans Boehm84614952014-11-25 18:46:17 -08001196 public void onAnimationEnd(Animator animation) {
1197 setState(CalculatorState.RESULT);
1198 mCurrentAnimator = null;
1199 }
1200 });
Justin Klaassen4b3af052014-05-27 17:53:10 -07001201
Hans Boehm84614952014-11-25 18:46:17 -08001202 mCurrentAnimator = animatorSet;
1203 animatorSet.start();
Hans Boehm8f051c32016-10-03 16:53:58 -07001204 } else /* No animation desired; get there fast when restarting */ {
Justin Klaassen44595162015-05-28 17:55:20 -07001205 mResultText.setScaleX(resultScale);
1206 mResultText.setScaleY(resultScale);
1207 mResultText.setTranslationY(resultTranslationY);
1208 mResultText.setTextColor(formulaTextColor);
Annie Chine918fd22016-03-09 11:07:54 -08001209 mFormulaContainer.setTranslationY(formulaTranslationY);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001210 setState(CalculatorState.RESULT);
Hans Boehm84614952014-11-25 18:46:17 -08001211 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001212 }
Hans Boehm84614952014-11-25 18:46:17 -08001213
1214 // Restore positions of the formula and result displays back to their original,
1215 // pre-animation state.
1216 private void restoreDisplayPositions() {
1217 // Clear result.
Justin Klaassen44595162015-05-28 17:55:20 -07001218 mResultText.setText("");
Hans Boehm84614952014-11-25 18:46:17 -08001219 // Reset all of the values modified during the animation.
Justin Klaassen44595162015-05-28 17:55:20 -07001220 mResultText.setScaleX(1.0f);
1221 mResultText.setScaleY(1.0f);
1222 mResultText.setTranslationX(0.0f);
1223 mResultText.setTranslationY(0.0f);
Annie Chine918fd22016-03-09 11:07:54 -08001224 mFormulaContainer.setTranslationY(0.0f);
Hans Boehm84614952014-11-25 18:46:17 -08001225
Hans Boehm08e8f322015-04-21 13:18:38 -07001226 mFormulaText.requestFocus();
Hans Boehm5e6a0ca2015-09-22 17:09:01 -07001227 }
1228
1229 @Override
1230 public void onClick(AlertDialogFragment fragment, int which) {
1231 if (which == DialogInterface.BUTTON_POSITIVE) {
Christine Frankscbc51fa2017-01-04 21:00:36 -08001232 if (HistoryFragment.CLEAR_DIALOG_TAG.equals(fragment.getTag())) {
Annie Chin532b77e2016-12-06 13:30:35 -08001233 // TODO: Try to preserve the current, saved, and memory expressions. How should we
1234 // handle expressions to which they refer?
Annie Chin532b77e2016-12-06 13:30:35 -08001235 mEvaluator.clearEverything();
1236 // TODO: It's not clear what we should really do here. This is an initial hack.
1237 // May want to make onClearAnimationEnd() private if/when we fix this.
1238 onClearAnimationEnd();
Christine Frankscbc51fa2017-01-04 21:00:36 -08001239 mEvaluatorCallback.onMemoryStateChanged();
Annie Chin532b77e2016-12-06 13:30:35 -08001240 onBackPressed();
Christine Frankscbc51fa2017-01-04 21:00:36 -08001241 } else if (Evaluator.TIMEOUT_DIALOG_TAG.equals(fragment.getTag())) {
Annie Chin532b77e2016-12-06 13:30:35 -08001242 // Timeout extension request.
1243 mEvaluator.setLongTimeout();
1244 } else {
1245 Log.e(TAG, "Unknown AlertDialogFragment click:" + fragment.getTag());
1246 }
Hans Boehm5e6a0ca2015-09-22 17:09:01 -07001247 }
1248 }
Hans Boehm84614952014-11-25 18:46:17 -08001249
Justin Klaassend48b7562015-04-16 16:51:38 -07001250 @Override
1251 public boolean onCreateOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -07001252 super.onCreateOptionsMenu(menu);
1253
1254 getMenuInflater().inflate(R.menu.activity_calculator, menu);
Justin Klaassend48b7562015-04-16 16:51:38 -07001255 return true;
1256 }
1257
1258 @Override
1259 public boolean onPrepareOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -07001260 super.onPrepareOptionsMenu(menu);
1261
1262 // Show the leading option when displaying a result.
1263 menu.findItem(R.id.menu_leading).setVisible(mCurrentState == CalculatorState.RESULT);
1264
1265 // Show the fraction option when displaying a rational result.
Annie Chin52ce1292017-02-02 12:55:14 -08001266 boolean visible = mCurrentState == CalculatorState.RESULT;
1267 final UnifiedReal mainResult = mEvaluator.getResult(Evaluator.MAIN_INDEX);
1268 // mainResult should never be null, but it happens. Check as a workaround to protect
1269 // against crashes until we find the root cause (b/34763650).
1270 visible &= mainResult != null && mainResult.exactlyDisplayable();
1271 menu.findItem(R.id.menu_fraction).setVisible(visible);
Justin Klaassend36d63e2015-05-05 12:59:36 -07001272
Justin Klaassend48b7562015-04-16 16:51:38 -07001273 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001274 }
1275
1276 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -07001277 public boolean onOptionsItemSelected(MenuItem item) {
Hans Boehm84614952014-11-25 18:46:17 -08001278 switch (item.getItemId()) {
Annie Chinabd202f2016-10-14 14:23:45 -07001279 case R.id.menu_history:
Annie China8b31db2017-02-09 08:11:22 -08001280 showHistoryFragment();
Annie Chinabd202f2016-10-14 14:23:45 -07001281 return true;
Justin Klaassend36d63e2015-05-05 12:59:36 -07001282 case R.id.menu_leading:
1283 displayFull();
Hans Boehm84614952014-11-25 18:46:17 -08001284 return true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001285 case R.id.menu_fraction:
1286 displayFraction();
1287 return true;
Justin Klaassend36d63e2015-05-05 12:59:36 -07001288 case R.id.menu_licenses:
1289 startActivity(new Intent(this, Licenses.class));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001290 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001291 default:
1292 return super.onOptionsItemSelected(item);
1293 }
1294 }
1295
Annie Chin52eed7a2017-01-04 15:06:05 -08001296 /* Begin override CloseCallback method. */
1297
1298 @Override
1299 public void onClose() {
1300 removeHistoryFragment();
1301 }
1302
1303 /* End override CloseCallback method. */
1304
1305 /* Begin override DragCallback methods */
1306
Annie Chin52eed7a2017-01-04 15:06:05 -08001307 public void onStartDraggingOpen() {
Justin Klaassen39297782016-12-19 09:11:38 -08001308 mDisplayView.hideToolbar();
Annie China8b31db2017-02-09 08:11:22 -08001309 showHistoryFragment();
Justin Klaassen39297782016-12-19 09:11:38 -08001310 }
1311
1312 @Override
1313 public void onInstanceStateRestored(boolean isOpen) {
Annie Chin52eed7a2017-01-04 15:06:05 -08001314 }
1315
1316 @Override
1317 public void whileDragging(float yFraction) {
Annie Chin52eed7a2017-01-04 15:06:05 -08001318 }
1319
1320 @Override
1321 public boolean shouldCaptureView(View view, int x, int y) {
Justin Klaassen39297782016-12-19 09:11:38 -08001322 return view.getId() == R.id.history_frame
1323 && (mDragLayout.isMoving() || mDragLayout.isViewUnder(view, x, y));
Annie Chin52eed7a2017-01-04 15:06:05 -08001324 }
1325
1326 @Override
1327 public int getDisplayHeight() {
1328 return mDisplayView.getMeasuredHeight();
1329 }
1330
Annie Chin52eed7a2017-01-04 15:06:05 -08001331 /* End override DragCallback methods */
1332
Hans Boehm31ea2522016-11-23 17:47:02 -08001333 /**
1334 * Change evaluation state to one that's friendly to the history fragment.
1335 * Return false if that was not easily possible.
1336 */
1337 private boolean prepareForHistory() {
1338 if (mCurrentState == CalculatorState.ANIMATE) {
Annie Chinbdfd38c2017-11-02 16:47:43 -07001339 // End the current animation and signal that preparation has failed.
1340 // onUserInteraction is unreliable and onAnimationEnd() is asynchronous, so we
1341 // aren't guaranteed to be out of the ANIMATE state by the time prepareForHistory is
1342 // called.
1343 if (mCurrentAnimator != null) {
1344 mCurrentAnimator.end();
1345 }
1346 return false;
Annie Chine5567fd2016-12-12 13:45:24 -08001347 } else if (mCurrentState == CalculatorState.EVALUATE) {
1348 // Cancel current evaluation
1349 cancelIfEvaluating(true /* quiet */ );
1350 setState(CalculatorState.INPUT);
1351 return true;
1352 } else if (mCurrentState == CalculatorState.INIT) {
Hans Boehm31ea2522016-11-23 17:47:02 -08001353 // Easiest to just refuse. Otherwise we can see a state change
1354 // while in history mode, which causes all sorts of problems.
1355 // TODO: Consider other alternatives. If we're just doing the decimal conversion
1356 // at the end of an evaluation, we could treat this as RESULT state.
1357 return false;
1358 }
1359 // We should be in INPUT, INIT_FOR_RESULT, RESULT, or ERROR state.
1360 return true;
1361 }
1362
Justin Klaassen39297782016-12-19 09:11:38 -08001363 private HistoryFragment getHistoryFragment() {
1364 final FragmentManager manager = getFragmentManager();
1365 if (manager == null || manager.isDestroyed()) {
1366 return null;
1367 }
Annie China8b31db2017-02-09 08:11:22 -08001368 final Fragment fragment = manager.findFragmentByTag(HistoryFragment.TAG);
1369 return fragment == null || fragment.isRemoving() ? null : (HistoryFragment) fragment;
Justin Klaassen39297782016-12-19 09:11:38 -08001370 }
1371
Annie China8b31db2017-02-09 08:11:22 -08001372 private void showHistoryFragment() {
Annie Chinbdfd38c2017-11-02 16:47:43 -07001373 if (getHistoryFragment() != null) {
1374 // If the fragment already exists, do nothing.
Annie Chin06fd3cf2016-11-07 16:04:33 -08001375 return;
1376 }
Justin Klaassen39297782016-12-19 09:11:38 -08001377
Annie Chinbdfd38c2017-11-02 16:47:43 -07001378 final FragmentManager manager = getFragmentManager();
1379 if (manager == null || manager.isDestroyed() || !prepareForHistory()) {
1380 // If the history fragment can not be shown, close the draglayout.
1381 mDragLayout.setClosed();
Hans Boehm31ea2522016-11-23 17:47:02 -08001382 return;
1383 }
Christine Franks7485df52016-12-01 13:18:45 -08001384
Justin Klaassen39297782016-12-19 09:11:38 -08001385 stopActionModeOrContextMenu();
1386 manager.beginTransaction()
1387 .replace(R.id.history_frame, new HistoryFragment(), HistoryFragment.TAG)
Annie China8b31db2017-02-09 08:11:22 -08001388 .setTransition(FragmentTransaction.TRANSIT_NONE)
Justin Klaassen39297782016-12-19 09:11:38 -08001389 .addToBackStack(HistoryFragment.TAG)
1390 .commit();
Annie Chineb36f952016-12-08 17:27:19 -08001391
Justin Klaassen39297782016-12-19 09:11:38 -08001392 // When HistoryFragment is visible, hide all descendants of the main Calculator view.
1393 mMainCalculator.setImportantForAccessibility(
1394 View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
Annie Chin06fd3cf2016-11-07 16:04:33 -08001395 // TODO: pass current scroll position of result
Annie Chin09547532016-10-14 10:59:07 -07001396 }
1397
Christine Franks7452d3a2016-10-27 13:41:18 -07001398 private void displayMessage(String title, String message) {
Annie Chin532b77e2016-12-06 13:30:35 -08001399 AlertDialogFragment.showMessageDialog(this, title, message, null, null /* tag */);
Hans Boehm84614952014-11-25 18:46:17 -08001400 }
1401
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001402 private void displayFraction() {
Hans Boehm8f051c32016-10-03 16:53:58 -07001403 UnifiedReal result = mEvaluator.getResult(Evaluator.MAIN_INDEX);
Christine Franks7452d3a2016-10-27 13:41:18 -07001404 displayMessage(getString(R.string.menu_fraction),
1405 KeyMaps.translateResult(result.toNiceString()));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001406 }
1407
1408 // Display full result to currently evaluated precision
1409 private void displayFull() {
1410 Resources res = getResources();
Hans Boehm24c91ed2016-06-30 18:53:44 -07001411 String msg = mResultText.getFullText(true /* withSeparators */) + " ";
Justin Klaassen44595162015-05-28 17:55:20 -07001412 if (mResultText.fullTextIsExact()) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001413 msg += res.getString(R.string.exact);
1414 } else {
1415 msg += res.getString(R.string.approximate);
1416 }
Christine Franks7452d3a2016-10-27 13:41:18 -07001417 displayMessage(getString(R.string.menu_leading), msg);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001418 }
1419
Hans Boehm017de982015-06-10 17:46:03 -07001420 /**
1421 * Add input characters to the end of the expression.
1422 * Map them to the appropriate button pushes when possible. Leftover characters
1423 * are added to mUnprocessedChars, which is presumed to immediately precede the newly
1424 * added characters.
Hans Boehm65a99a42016-02-03 18:16:07 -08001425 * @param moreChars characters to be added
1426 * @param explicit these characters were explicitly typed by the user, not pasted
Hans Boehm017de982015-06-10 17:46:03 -07001427 */
1428 private void addChars(String moreChars, boolean explicit) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001429 if (mUnprocessedChars != null) {
1430 moreChars = mUnprocessedChars + moreChars;
1431 }
1432 int current = 0;
1433 int len = moreChars.length();
Hans Boehm0b9806f2015-06-29 16:07:15 -07001434 boolean lastWasDigit = false;
Hans Boehm5d79d102015-09-16 16:33:47 -07001435 if (mCurrentState == CalculatorState.RESULT && len != 0) {
1436 // Clear display immediately for incomplete function name.
1437 switchToInput(KeyMaps.keyForChar(moreChars.charAt(current)));
1438 }
Hans Boehm24c91ed2016-06-30 18:53:44 -07001439 char groupingSeparator = KeyMaps.translateResult(",").charAt(0);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001440 while (current < len) {
1441 char c = moreChars.charAt(current);
Hans Boehm24c91ed2016-06-30 18:53:44 -07001442 if (Character.isSpaceChar(c) || c == groupingSeparator) {
1443 ++current;
1444 continue;
1445 }
Hans Boehm013969e2015-04-13 20:29:47 -07001446 int k = KeyMaps.keyForChar(c);
Hans Boehm0b9806f2015-06-29 16:07:15 -07001447 if (!explicit) {
1448 int expEnd;
1449 if (lastWasDigit && current !=
1450 (expEnd = Evaluator.exponentEnd(moreChars, current))) {
1451 // Process scientific notation with 'E' when pasting, in spite of ambiguity
1452 // with base of natural log.
1453 // Otherwise the 10^x key is the user's friend.
1454 mEvaluator.addExponent(moreChars, current, expEnd);
1455 current = expEnd;
1456 lastWasDigit = false;
1457 continue;
1458 } else {
1459 boolean isDigit = KeyMaps.digVal(k) != KeyMaps.NOT_DIGIT;
1460 if (current == 0 && (isDigit || k == R.id.dec_point)
Hans Boehm8f051c32016-10-03 16:53:58 -07001461 && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasTrailingConstant()) {
Hans Boehm0b9806f2015-06-29 16:07:15 -07001462 // Refuse to concatenate pasted content to trailing constant.
1463 // This makes pasting of calculator results more consistent, whether or
1464 // not the old calculator instance is still around.
1465 addKeyToExpr(R.id.op_mul);
1466 }
1467 lastWasDigit = (isDigit || lastWasDigit && k == R.id.dec_point);
1468 }
1469 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001470 if (k != View.NO_ID) {
1471 mCurrentButton = findViewById(k);
Hans Boehm017de982015-06-10 17:46:03 -07001472 if (explicit) {
1473 addExplicitKeyToExpr(k);
1474 } else {
1475 addKeyToExpr(k);
1476 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001477 if (Character.isSurrogate(c)) {
1478 current += 2;
1479 } else {
1480 ++current;
1481 }
1482 continue;
1483 }
Hans Boehm013969e2015-04-13 20:29:47 -07001484 int f = KeyMaps.funForString(moreChars, current);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001485 if (f != View.NO_ID) {
1486 mCurrentButton = findViewById(f);
Hans Boehm017de982015-06-10 17:46:03 -07001487 if (explicit) {
1488 addExplicitKeyToExpr(f);
1489 } else {
1490 addKeyToExpr(f);
1491 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001492 if (f == R.id.op_sqrt) {
1493 // Square root entered as function; don't lose the parenthesis.
1494 addKeyToExpr(R.id.lparen);
1495 }
1496 current = moreChars.indexOf('(', current) + 1;
1497 continue;
1498 }
1499 // There are characters left, but we can't convert them to button presses.
1500 mUnprocessedChars = moreChars.substring(current);
1501 redisplayAfterFormulaChange();
Hans Boehm52d477a2016-04-01 17:42:50 -07001502 showOrHideToolbar();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001503 return;
1504 }
1505 mUnprocessedChars = null;
1506 redisplayAfterFormulaChange();
Hans Boehm52d477a2016-04-01 17:42:50 -07001507 showOrHideToolbar();
Hans Boehm84614952014-11-25 18:46:17 -08001508 }
1509
Hans Boehm8f051c32016-10-03 16:53:58 -07001510 private void clearIfNotInputState() {
1511 if (mCurrentState == CalculatorState.ERROR
1512 || mCurrentState == CalculatorState.RESULT) {
1513 setState(CalculatorState.INPUT);
1514 mEvaluator.clearMain();
1515 }
1516 }
1517
Chenjie Yu3937b652016-06-01 23:14:26 -07001518 /**
Christine Franksbd90b792016-11-22 10:28:26 -08001519 * Since we only support LTR format, using the RTL comma does not make sense.
1520 */
1521 private String getDecimalSeparator() {
1522 final char defaultSeparator = DecimalFormatSymbols.getInstance().getDecimalSeparator();
1523 final char rtlComma = '\u066b';
1524 return defaultSeparator == rtlComma ? "," : String.valueOf(defaultSeparator);
1525 }
1526
1527 /**
Chenjie Yu3937b652016-06-01 23:14:26 -07001528 * Clean up animation for context menu.
1529 */
1530 @Override
1531 public void onContextMenuClosed(Menu menu) {
1532 stopActionModeOrContextMenu();
1533 }
Christine Franks1d99be12016-11-14 14:00:36 -08001534
1535 public interface OnDisplayMemoryOperationsListener {
1536 boolean shouldDisplayMemory();
1537 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001538}