1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
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 
17 // 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.
25 
26 package com.android.calculator2;
27 
28 import android.animation.Animator;
29 import android.animation.Animator.AnimatorListener;
30 import android.animation.AnimatorListenerAdapter;
31 import android.animation.AnimatorSet;
32 import android.animation.ObjectAnimator;
33 import android.animation.PropertyValuesHolder;
34 import android.app.ActionBar;
35 import android.app.Activity;
36 import android.app.Fragment;
37 import android.app.FragmentManager;
38 import android.app.FragmentTransaction;
39 import android.content.ClipData;
40 import android.content.DialogInterface;
41 import android.content.Intent;
42 import android.content.res.Resources;
43 import android.graphics.Color;
44 import android.graphics.Rect;
45 import android.net.Uri;
46 import android.os.Bundle;
47 import android.support.annotation.NonNull;
48 import android.support.annotation.StringRes;
49 import android.support.v4.content.ContextCompat;
50 import android.support.v4.view.ViewPager;
51 import android.text.Editable;
52 import android.text.SpannableStringBuilder;
53 import android.text.Spanned;
54 import android.text.TextUtils;
55 import android.text.TextWatcher;
56 import android.text.style.ForegroundColorSpan;
57 import android.util.Log;
58 import android.util.Property;
59 import android.view.ActionMode;
60 import android.view.KeyCharacterMap;
61 import android.view.KeyEvent;
62 import android.view.Menu;
63 import android.view.MenuItem;
64 import android.view.MotionEvent;
65 import android.view.View;
66 import android.view.View.OnLongClickListener;
67 import android.view.ViewAnimationUtils;
68 import android.view.ViewGroupOverlay;
69 import android.view.ViewTreeObserver;
70 import android.view.animation.AccelerateDecelerateInterpolator;
71 import android.widget.HorizontalScrollView;
72 import android.widget.TextView;
73 import android.widget.Toolbar;
74 
75 import com.android.calculator2.CalculatorFormula.OnTextSizeChangeListener;
76 
77 import java.io.ByteArrayInputStream;
78 import java.io.ByteArrayOutputStream;
79 import java.io.IOException;
80 import java.io.ObjectInput;
81 import java.io.ObjectInputStream;
82 import java.io.ObjectOutput;
83 import java.io.ObjectOutputStream;
84 import java.text.DecimalFormatSymbols;
85 
86 import static com.android.calculator2.CalculatorFormula.OnFormulaContextMenuClickListener;
87 
88 public class Calculator extends Activity
89         implements OnTextSizeChangeListener, OnLongClickListener,
90         AlertDialogFragment.OnClickListener, Evaluator.EvaluationListener /* for main result */,
91         DragLayout.CloseCallback, DragLayout.DragCallback {
92 
93     private static final String TAG = "Calculator";
94     /**
95      * Constant for an invalid resource id.
96      */
97     public static final int INVALID_RES_ID = -1;
98 
99     private enum CalculatorState {
100         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.
103                         // Not used for instant result evaluation.
104         INIT,           // Very temporary state used as alternative to EVALUATE
105                         // during reinitialization.  Do not animate on completion.
106         INIT_FOR_RESULT,  // Identical to INIT, but evaluation is known to terminate
107                           // with result, and current expression has been copied to history.
108         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.
112                         // The current formula is now also the last history entry.
113         ERROR           // Error displayed: Formula visible, result shows error message.
114                         // Display similar to INPUT state.
115     }
116     // 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.
123     // ERROR and RESULT are translated to INIT or INIT_FOR_RESULT state if the application
124     // is restarted in that state.  This leads us to recompute and redisplay the result
125     // 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.
129     // TODO: Possibly save a bit more information, e.g. its initial display string
130     // or most significant digit position, to speed up restart.
131 
132     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 
145     private static final String NAME = "Calculator";
146     private static final String KEY_DISPLAY_STATE = NAME + "_display_state";
147     private static final String KEY_UNPROCESSED_CHARS = NAME + "_unprocessed_chars";
148     /**
149      * Associated value is a byte array holding the evaluator state.
150      */
151     private static final String KEY_EVAL_STATE = NAME + "_eval_state";
152     private static final String KEY_INVERSE_MODE = NAME + "_inverse_mode";
153     /**
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";
157 
158     private final ViewTreeObserver.OnPreDrawListener mPreDrawListener =
159             new ViewTreeObserver.OnPreDrawListener() {
160         @Override
161         public boolean onPreDraw() {
162             mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
163             final ViewTreeObserver observer = mFormulaContainer.getViewTreeObserver();
164             if (observer.isAlive()) {
165                 observer.removeOnPreDrawListener(this);
166             }
167             return false;
168         }
169     };
170 
171     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 =
187             new OnDisplayMemoryOperationsListener() {
188         @Override
189         public boolean shouldDisplayMemory() {
190             return mEvaluator.getMemoryIndex() != 0;
191         }
192     };
193 
194     private final OnFormulaContextMenuClickListener mOnFormulaContextMenuClickListener =
195             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();
223             }
224         }
225     };
226 
227 
228     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 
247     private CalculatorState mCurrentState;
248     private Evaluator mEvaluator;
249 
250     private CalculatorDisplay mDisplayView;
251     private TextView mModeView;
252     private CalculatorFormula mFormulaText;
253     private CalculatorResult mResultText;
254     private HorizontalScrollView mFormulaContainer;
255     private DragLayout mDragLayout;
256 
257     private ViewPager mPadViewPager;
258     private View mDeleteButton;
259     private View mClearButton;
260     private View mEqualButton;
261     private View mMainCalculator;
262 
263     private TextView mInverseToggle;
264     private TextView mModeToggle;
265 
266     private View[] mInvertibleButtons;
267     private View[] mInverseButtons;
268 
269     private View mCurrentButton;
270     private Animator mCurrentAnimator;
271 
272     // 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);
279 
280     // Whether the display is one line.
281     private boolean mIsOneLine;
282 
283     /**
284      * Map the old saved state to a new state reflecting requested result reevaluation.
285      */
mapFromSaved(CalculatorState savedState)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     /**
304      * Restore Evaluator state and mCurrentState from savedInstanceState.
305      * Return true if the toolbar should be visible.
306      */
restoreInstanceState(Bundle savedInstanceState)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 
restoreDisplay()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.
349             setState(mapFromSaved(mCurrentState));
350             // Request evaluation when we know display width.
351             mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_REQUIRE, this);
352         }
353     }
354 
355     @Override
onCreate(Bundle savedInstanceState)356     protected void onCreate(Bundle savedInstanceState) {
357         super.onCreate(savedInstanceState);
358 
359         setContentView(R.layout.activity_calculator_main);
360         setActionBar((Toolbar) findViewById(R.id.toolbar));
361 
362         // Hide all default options in the ActionBar.
363         getActionBar().setDisplayOptions(0);
364 
365         // 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 
373         mMainCalculator = findViewById(R.id.main_calculator);
374         mDisplayView = (CalculatorDisplay) findViewById(R.id.display);
375         mModeView = (TextView) findViewById(R.id.mode);
376         mFormulaText = (CalculatorFormula) findViewById(R.id.formula);
377         mResultText = (CalculatorResult) findViewById(R.id.result);
378         mFormulaContainer = (HorizontalScrollView) findViewById(R.id.formula_container);
379         mEvaluator = Evaluator.getInstance(this);
380         mEvaluator.setCallback(mEvaluatorCallback);
381         mResultText.setEvaluator(mEvaluator, Evaluator.MAIN_INDEX);
382         KeyMaps.setActivity(this);
383 
384         mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
385         mDeleteButton = findViewById(R.id.del);
386         mClearButton = findViewById(R.id.clr);
387         final View numberPad = findViewById(R.id.pad_numeric);
388         mEqualButton = numberPad.findViewById(R.id.eq);
389         if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) {
390             mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq);
391         }
392         final TextView decimalPointButton = (TextView) numberPad.findViewById(R.id.dec_point);
393         decimalPointButton.setText(getDecimalSeparator());
394 
395         mInverseToggle = (TextView) findViewById(R.id.toggle_inv);
396         mModeToggle = (TextView) findViewById(R.id.toggle_mode);
397 
398         mIsOneLine = mResultText.getVisibility() == View.INVISIBLE;
399 
400         mInvertibleButtons = new View[] {
401                 findViewById(R.id.fun_sin),
402                 findViewById(R.id.fun_cos),
403                 findViewById(R.id.fun_tan),
404                 findViewById(R.id.fun_ln),
405                 findViewById(R.id.fun_log),
406                 findViewById(R.id.op_sqrt)
407         };
408         mInverseButtons = new View[] {
409                 findViewById(R.id.fun_arcsin),
410                 findViewById(R.id.fun_arccos),
411                 findViewById(R.id.fun_arctan),
412                 findViewById(R.id.fun_exp),
413                 findViewById(R.id.fun_10pow),
414                 findViewById(R.id.op_sqr)
415         };
416 
417         mDragLayout = (DragLayout) findViewById(R.id.drag_layout);
418         mDragLayout.removeDragCallback(this);
419         mDragLayout.addDragCallback(this);
420         mDragLayout.setCloseCallback(this);
421 
422         mFormulaText.setOnContextMenuClickListener(mOnFormulaContextMenuClickListener);
423         mFormulaText.setOnDisplayMemoryOperationsListener(mOnDisplayMemoryOperationsListener);
424 
425         mFormulaText.setOnTextSizeChangeListener(this);
426         mFormulaText.addTextChangedListener(mFormulaTextWatcher);
427         mDeleteButton.setOnLongClickListener(this);
428 
429         if (savedInstanceState != null) {
430             restoreInstanceState(savedInstanceState);
431         } else {
432             mCurrentState = CalculatorState.INPUT;
433             mEvaluator.clearMain();
434             showAndMaybeHideToolbar();
435             onInverseToggled(false);
436         }
437         restoreDisplay();
438     }
439 
440     @Override
onResume()441     protected void onResume() {
442         super.onResume();
443         if (mDisplayView.isToolbarVisible()) {
444             showAndMaybeHideToolbar();
445         }
446         // 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);
454     }
455 
456     @Override
onSaveInstanceState(@onNull Bundle outState)457     protected void onSaveInstanceState(@NonNull Bundle outState) {
458         mEvaluator.cancelAll(true);
459         // 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 
464         super.onSaveInstanceState(outState);
465         outState.putInt(KEY_DISPLAY_STATE, mCurrentState.ordinal());
466         outState.putCharSequence(KEY_UNPROCESSED_CHARS, mUnprocessedChars);
467         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());
475         outState.putBoolean(KEY_INVERSE_MODE, mInverseToggle.isSelected());
476         outState.putBoolean(KEY_SHOW_TOOLBAR, mDisplayView.isToolbarVisible());
477         // We must wait for asynchronous writes to complete, since outState may contain
478         // references to expressions being written.
479         mEvaluator.waitForWrites();
480     }
481 
482     // Set the state, updating delete label and display colors.
483     // This restores display positions on moving to INPUT.
484     // But movement/animation for moving to RESULT has already been done.
setState(CalculatorState state)485     private void setState(CalculatorState state) {
486         if (mCurrentState != state) {
487             if (state == CalculatorState.INPUT) {
488                 // We'll explicitly request evaluation from now on.
489                 mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_NOT_EVALUATE, null);
490                 restoreDisplayPositions();
491             }
492             mCurrentState = state;
493 
494             if (mCurrentState == CalculatorState.RESULT) {
495                 // No longer do this for ERROR; allow mistakes to be corrected.
496                 mDeleteButton.setVisibility(View.GONE);
497                 mClearButton.setVisibility(View.VISIBLE);
498             } else {
499                 mDeleteButton.setVisibility(View.VISIBLE);
500                 mClearButton.setVisibility(View.GONE);
501             }
502 
503             if (mIsOneLine) {
504                 if (mCurrentState == CalculatorState.RESULT
505                         || mCurrentState == CalculatorState.EVALUATE
506                         || mCurrentState == CalculatorState.ANIMATE) {
507                     mFormulaText.setVisibility(View.VISIBLE);
508                     mResultText.setVisibility(View.VISIBLE);
509                 } else if (mCurrentState == CalculatorState.ERROR) {
510                     mFormulaText.setVisibility(View.INVISIBLE);
511                     mResultText.setVisibility(View.VISIBLE);
512                 } else {
513                     mFormulaText.setVisibility(View.VISIBLE);
514                     mResultText.setVisibility(View.INVISIBLE);
515                 }
516             }
517 
518             if (mCurrentState == CalculatorState.ERROR) {
519                 final int errorColor =
520                         ContextCompat.getColor(this, R.color.calculator_error_color);
521                 mFormulaText.setTextColor(errorColor);
522                 mResultText.setTextColor(errorColor);
523                 getWindow().setStatusBarColor(errorColor);
524             } else if (mCurrentState != CalculatorState.RESULT) {
525                 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(
530                         ContextCompat.getColor(this, R.color.calculator_statusbar_color));
531             }
532 
533             invalidateOptionsMenu();
534         }
535     }
536 
isResultLayout()537     public boolean isResultLayout() {
538         if (mCurrentState == CalculatorState.ANIMATE) {
539             throw new AssertionError("impossible state");
540         }
541         // Note that ERROR has INPUT, not RESULT layout.
542         return mCurrentState == CalculatorState.INIT_FOR_RESULT
543                 || mCurrentState == CalculatorState.RESULT;
544     }
545 
isOneLine()546     public boolean isOneLine() {
547         return mIsOneLine;
548     }
549 
550     @Override
onDestroy()551     protected void onDestroy() {
552         mDragLayout.removeDragCallback(this);
553         super.onDestroy();
554     }
555 
556     /**
557      * Destroy the evaluator and close the underlying database.
558      */
destroyEvaluator()559     public void destroyEvaluator() {
560         mEvaluator.destroyEvaluator();
561     }
562 
563     @Override
onActionModeStarted(ActionMode mode)564     public void onActionModeStarted(ActionMode mode) {
565         super.onActionModeStarted(mode);
566         if (mode.getTag() == CalculatorFormula.TAG_ACTION_MODE) {
567             mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
568         }
569     }
570 
571     /**
572      * Stop any active ActionMode or ContextMenu for copy/paste actions.
573      * Return true if there was one.
574      */
stopActionModeOrContextMenu()575     private boolean stopActionModeOrContextMenu() {
576         return mResultText.stopActionModeOrContextMenu()
577                 || mFormulaText.stopActionModeOrContextMenu();
578     }
579 
580     @Override
onUserInteraction()581     public void onUserInteraction() {
582         super.onUserInteraction();
583 
584         // If there's an animation in progress, end it immediately, so the user interaction can
585         // be handled.
586         if (mCurrentAnimator != null) {
587             mCurrentAnimator.end();
588         }
589     }
590 
591     @Override
dispatchTouchEvent(MotionEvent e)592     public boolean dispatchTouchEvent(MotionEvent e) {
593         if (e.getActionMasked() == MotionEvent.ACTION_DOWN) {
594             stopActionModeOrContextMenu();
595 
596             final HistoryFragment historyFragment = getHistoryFragment();
597             if (mDragLayout.isOpen() && historyFragment != null) {
598                 historyFragment.stopActionModeOrContextMenu();
599             }
600         }
601         return super.dispatchTouchEvent(e);
602     }
603 
604     @Override
onBackPressed()605     public void onBackPressed() {
606         if (!stopActionModeOrContextMenu()) {
607             final HistoryFragment historyFragment = getHistoryFragment();
608             if (mDragLayout.isOpen() && historyFragment != null) {
609                 if (!historyFragment.stopActionModeOrContextMenu()) {
610                     removeHistoryFragment();
611                 }
612                 return;
613             }
614             if (mPadViewPager != null && mPadViewPager.getCurrentItem() != 0) {
615                 // Select the previous pad.
616                 mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
617             } else {
618                 // If the user is currently looking at the first pad (or the pad is not paged),
619                 // allow the system to handle the Back button.
620                 super.onBackPressed();
621             }
622         }
623     }
624 
625     @Override
onKeyUp(int keyCode, KeyEvent event)626     public boolean onKeyUp(int keyCode, KeyEvent event) {
627         // Allow the system to handle special key codes (e.g. "BACK" or "DPAD").
628         switch (keyCode) {
629             case KeyEvent.KEYCODE_BACK:
630             case KeyEvent.KEYCODE_ESCAPE:
631             case KeyEvent.KEYCODE_DPAD_UP:
632             case KeyEvent.KEYCODE_DPAD_DOWN:
633             case KeyEvent.KEYCODE_DPAD_LEFT:
634             case KeyEvent.KEYCODE_DPAD_RIGHT:
635                 return super.onKeyUp(keyCode, event);
636         }
637 
638         // Stop the action mode or context menu if it's showing.
639         stopActionModeOrContextMenu();
640 
641         // Always cancel unrequested in-progress evaluation of the main expression, so that
642         // we don't have to worry about subsequent asynchronous completion.
643         // Requested in-progress evaluations are handled below.
644         cancelUnrequested();
645 
646         switch (keyCode) {
647             case KeyEvent.KEYCODE_NUMPAD_ENTER:
648             case KeyEvent.KEYCODE_ENTER:
649             case KeyEvent.KEYCODE_DPAD_CENTER:
650                 mCurrentButton = mEqualButton;
651                 onEquals();
652                 return true;
653             case KeyEvent.KEYCODE_DEL:
654                 mCurrentButton = mDeleteButton;
655                 onDelete();
656                 return true;
657             case KeyEvent.KEYCODE_CLEAR:
658                 mCurrentButton = mClearButton;
659                 onClear();
660                 return true;
661             default:
662                 cancelIfEvaluating(false);
663                 final int raw = event.getKeyCharacterMap().get(keyCode, event.getMetaState());
664                 if ((raw & KeyCharacterMap.COMBINING_ACCENT) != 0) {
665                     return true; // discard
666                 }
667                 // Try to discard non-printing characters and the like.
668                 // The user will have to explicitly delete other junk that gets past us.
669                 if (Character.isIdentifierIgnorable(raw) || Character.isWhitespace(raw)) {
670                     return true;
671                 }
672                 char c = (char) raw;
673                 if (c == '=') {
674                     mCurrentButton = mEqualButton;
675                     onEquals();
676                 } else {
677                     addChars(String.valueOf(c), true);
678                     redisplayAfterFormulaChange();
679                 }
680                 return true;
681         }
682     }
683 
684     /**
685      * Invoked whenever the inverse button is toggled to update the UI.
686      *
687      * @param showInverse {@code true} if inverse functions should be shown
688      */
onInverseToggled(boolean showInverse)689     private void onInverseToggled(boolean showInverse) {
690         mInverseToggle.setSelected(showInverse);
691         if (showInverse) {
692             mInverseToggle.setContentDescription(getString(R.string.desc_inv_on));
693             for (View invertibleButton : mInvertibleButtons) {
694                 invertibleButton.setVisibility(View.GONE);
695             }
696             for (View inverseButton : mInverseButtons) {
697                 inverseButton.setVisibility(View.VISIBLE);
698             }
699         } else {
700             mInverseToggle.setContentDescription(getString(R.string.desc_inv_off));
701             for (View invertibleButton : mInvertibleButtons) {
702                 invertibleButton.setVisibility(View.VISIBLE);
703             }
704             for (View inverseButton : mInverseButtons) {
705                 inverseButton.setVisibility(View.GONE);
706             }
707         }
708     }
709 
710     /**
711      * Invoked whenever the deg/rad mode may have changed to update the UI. Note that the mode has
712      * not necessarily actually changed where this is invoked.
713      *
714      * @param degreeMode {@code true} if in degree mode
715      */
onModeChanged(boolean degreeMode)716     private void onModeChanged(boolean degreeMode) {
717         if (degreeMode) {
718             mModeView.setText(R.string.mode_deg);
719             mModeView.setContentDescription(getString(R.string.desc_mode_deg));
720 
721             mModeToggle.setText(R.string.mode_rad);
722             mModeToggle.setContentDescription(getString(R.string.desc_switch_rad));
723         } else {
724             mModeView.setText(R.string.mode_rad);
725             mModeView.setContentDescription(getString(R.string.desc_mode_rad));
726 
727             mModeToggle.setText(R.string.mode_deg);
728             mModeToggle.setContentDescription(getString(R.string.desc_switch_deg));
729         }
730     }
731 
removeHistoryFragment()732     private void removeHistoryFragment() {
733         final FragmentManager manager = getFragmentManager();
734         if (manager != null && !manager.isDestroyed()) {
735             manager.popBackStack(HistoryFragment.TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE);
736         }
737 
738         // When HistoryFragment is hidden, the main Calculator is important for accessibility again.
739         mMainCalculator.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
740     }
741 
742     /**
743      * Switch to INPUT from RESULT state in response to input of the specified button_id.
744      * View.NO_ID is treated as an incomplete function id.
745      */
switchToInput(int button_id)746     private void switchToInput(int button_id) {
747         if (KeyMaps.isBinary(button_id) || KeyMaps.isSuffix(button_id)) {
748             mEvaluator.collapse(mEvaluator.getMaxIndex() /* Most recent history entry */);
749         } else {
750             announceClearedForAccessibility();
751             mEvaluator.clearMain();
752         }
753         setState(CalculatorState.INPUT);
754     }
755 
756     // Add the given button id to input expression.
757     // If appropriate, clear the expression before doing so.
addKeyToExpr(int id)758     private void addKeyToExpr(int id) {
759         if (mCurrentState == CalculatorState.ERROR) {
760             setState(CalculatorState.INPUT);
761         } else if (mCurrentState == CalculatorState.RESULT) {
762             switchToInput(id);
763         }
764         if (!mEvaluator.append(id)) {
765             // TODO: Some user visible feedback?
766         }
767     }
768 
769     /**
770      * Add the given button id to input expression, assuming it was explicitly
771      * typed/touched.
772      * We perform slightly more aggressive correction than in pasted expressions.
773      */
addExplicitKeyToExpr(int id)774     private void addExplicitKeyToExpr(int id) {
775         if (mCurrentState == CalculatorState.INPUT && id == R.id.op_sub) {
776             mEvaluator.getExpr(Evaluator.MAIN_INDEX).removeTrailingAdditiveOperators();
777         }
778         addKeyToExpr(id);
779     }
780 
evaluateInstantIfNecessary()781     public void evaluateInstantIfNecessary() {
782         if (mCurrentState == CalculatorState.INPUT
783                 && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
784             mEvaluator.evaluateAndNotify(Evaluator.MAIN_INDEX, this, mResultText);
785         }
786     }
787 
redisplayAfterFormulaChange()788     private void redisplayAfterFormulaChange() {
789         // TODO: Could do this more incrementally.
790         redisplayFormula();
791         setState(CalculatorState.INPUT);
792         mResultText.clear();
793         if (haveUnprocessed()) {
794             // Force reevaluation when text is deleted, even if expression is unchanged.
795             mEvaluator.touch();
796         } else {
797             evaluateInstantIfNecessary();
798         }
799     }
800 
801     /**
802      * Show the toolbar.
803      * Automatically hide it again if it's not relevant to current formula.
804      */
showAndMaybeHideToolbar()805     private void showAndMaybeHideToolbar() {
806         final boolean shouldBeVisible =
807                 mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
808         mDisplayView.showToolbar(!shouldBeVisible);
809     }
810 
811     /**
812      * Display or hide the toolbar depending on calculator state.
813      */
showOrHideToolbar()814     private void showOrHideToolbar() {
815         final boolean shouldBeVisible =
816                 mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
817         if (shouldBeVisible) {
818             mDisplayView.showToolbar(false);
819         } else {
820             mDisplayView.hideToolbar();
821         }
822     }
823 
onButtonClick(View view)824     public void onButtonClick(View view) {
825         // Any animation is ended before we get here.
826         mCurrentButton = view;
827         stopActionModeOrContextMenu();
828 
829         // See onKey above for the rationale behind some of the behavior below:
830         cancelUnrequested();
831 
832         final int id = view.getId();
833         switch (id) {
834             case R.id.eq:
835                 onEquals();
836                 break;
837             case R.id.del:
838                 onDelete();
839                 break;
840             case R.id.clr:
841                 onClear();
842                 return;  // Toolbar visibility adjusted at end of animation.
843             case R.id.toggle_inv:
844                 final boolean selected = !mInverseToggle.isSelected();
845                 mInverseToggle.setSelected(selected);
846                 onInverseToggled(selected);
847                 if (mCurrentState == CalculatorState.RESULT) {
848                     mResultText.redisplay();   // In case we cancelled reevaluation.
849                 }
850                 break;
851             case R.id.toggle_mode:
852                 cancelIfEvaluating(false);
853                 final boolean mode = !mEvaluator.getDegreeMode(Evaluator.MAIN_INDEX);
854                 if (mCurrentState == CalculatorState.RESULT
855                         && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasTrigFuncs()) {
856                     // Capture current result evaluated in old mode.
857                     mEvaluator.collapse(mEvaluator.getMaxIndex());
858                     redisplayFormula();
859                 }
860                 // In input mode, we reinterpret already entered trig functions.
861                 mEvaluator.setDegreeMode(mode);
862                 onModeChanged(mode);
863                 // Show the toolbar to highlight the mode change.
864                 showAndMaybeHideToolbar();
865                 setState(CalculatorState.INPUT);
866                 mResultText.clear();
867                 if (!haveUnprocessed()) {
868                     evaluateInstantIfNecessary();
869                 }
870                 return;
871             default:
872                 cancelIfEvaluating(false);
873                 if (haveUnprocessed()) {
874                     // For consistency, append as uninterpreted characters.
875                     // This may actually be useful for a left parenthesis.
876                     addChars(KeyMaps.toString(this, id), true);
877                 } else {
878                     addExplicitKeyToExpr(id);
879                     redisplayAfterFormulaChange();
880                 }
881                 break;
882         }
883         showOrHideToolbar();
884     }
885 
redisplayFormula()886     void redisplayFormula() {
887         SpannableStringBuilder formula
888                 = mEvaluator.getExpr(Evaluator.MAIN_INDEX).toSpannableStringBuilder(this);
889         if (mUnprocessedChars != null) {
890             // Add and highlight characters we couldn't process.
891             formula.append(mUnprocessedChars, mUnprocessedColorSpan,
892                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
893         }
894         mFormulaText.changeTextTo(formula);
895         mFormulaText.setContentDescription(TextUtils.isEmpty(formula)
896                 ? getString(R.string.desc_formula) : null);
897     }
898 
899     @Override
onLongClick(View view)900     public boolean onLongClick(View view) {
901         mCurrentButton = view;
902 
903         if (view.getId() == R.id.del) {
904             onClear();
905             return true;
906         }
907         return false;
908     }
909 
910     // Initial evaluation completed successfully.  Initiate display.
onEvaluate(long index, int initDisplayPrec, int msd, int leastDigPos, String truncatedWholeNumber)911     public void onEvaluate(long index, int initDisplayPrec, int msd, int leastDigPos,
912             String truncatedWholeNumber) {
913         if (index != Evaluator.MAIN_INDEX) {
914             throw new AssertionError("Unexpected evaluation result index\n");
915         }
916 
917         // Invalidate any options that may depend on the current result.
918         invalidateOptionsMenu();
919 
920         mResultText.onEvaluate(index, initDisplayPrec, msd, leastDigPos, truncatedWholeNumber);
921         if (mCurrentState != CalculatorState.INPUT) {
922             // In EVALUATE, INIT, RESULT, or INIT_FOR_RESULT state.
923             onResult(mCurrentState == CalculatorState.EVALUATE /* animate */,
924                      mCurrentState == CalculatorState.INIT_FOR_RESULT
925                     || mCurrentState == CalculatorState.RESULT /* previously preserved */);
926         }
927     }
928 
929     // Reset state to reflect evaluator cancellation.  Invoked by evaluator.
onCancelled(long index)930     public void onCancelled(long index) {
931         // Index is Evaluator.MAIN_INDEX. We should be in EVALUATE state.
932         setState(CalculatorState.INPUT);
933         mResultText.onCancelled(index);
934     }
935 
936     // Reevaluation completed; ask result to redisplay current value.
onReevaluate(long index)937     public void onReevaluate(long index) {
938         // Index is Evaluator.MAIN_INDEX.
939         mResultText.onReevaluate(index);
940     }
941 
942     @Override
onTextSizeChanged(final TextView textView, float oldSize)943     public void onTextSizeChanged(final TextView textView, float oldSize) {
944         if (mCurrentState != CalculatorState.INPUT) {
945             // Only animate text changes that occur from user input.
946             return;
947         }
948 
949         // Calculate the values needed to perform the scale and translation animations,
950         // maintaining the same apparent baseline for the displayed text.
951         final float textScale = oldSize / textView.getTextSize();
952         final float translationX = (1.0f - textScale) *
953                 (textView.getWidth() / 2.0f - textView.getPaddingEnd());
954         final float translationY = (1.0f - textScale) *
955                 (textView.getHeight() / 2.0f - textView.getPaddingBottom());
956 
957         final AnimatorSet animatorSet = new AnimatorSet();
958         animatorSet.playTogether(
959                 ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f),
960                 ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f),
961                 ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f),
962                 ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f));
963         animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
964         animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
965         animatorSet.start();
966     }
967 
968     /**
969      * Cancel any in-progress explicitly requested evaluations.
970      * @param quiet suppress pop-up message.  Explicit evaluation can change the expression
971                     value, and certainly changes the display, so it seems reasonable to warn.
972      * @return      true if there was such an evaluation
973      */
cancelIfEvaluating(boolean quiet)974     private boolean cancelIfEvaluating(boolean quiet) {
975         if (mCurrentState == CalculatorState.EVALUATE) {
976             mEvaluator.cancel(Evaluator.MAIN_INDEX, quiet);
977             return true;
978         } else {
979             return false;
980         }
981     }
982 
983 
cancelUnrequested()984     private void cancelUnrequested() {
985         if (mCurrentState == CalculatorState.INPUT) {
986             mEvaluator.cancel(Evaluator.MAIN_INDEX, true);
987         }
988     }
989 
haveUnprocessed()990     private boolean haveUnprocessed() {
991         return mUnprocessedChars != null && !mUnprocessedChars.isEmpty();
992     }
993 
onEquals()994     private void onEquals() {
995         // Ignore if in non-INPUT state, or if there are no operators.
996         if (mCurrentState == CalculatorState.INPUT) {
997             if (haveUnprocessed()) {
998                 setState(CalculatorState.EVALUATE);
999                 onError(Evaluator.MAIN_INDEX, R.string.error_syntax);
1000             } else if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
1001                 setState(CalculatorState.EVALUATE);
1002                 mEvaluator.requireResult(Evaluator.MAIN_INDEX, this, mResultText);
1003             }
1004         }
1005     }
1006 
onDelete()1007     private void onDelete() {
1008         // Delete works like backspace; remove the last character or operator from the expression.
1009         // Note that we handle keyboard delete exactly like the delete button.  For
1010         // example the delete button can be used to delete a character from an incomplete
1011         // function name typed on a physical keyboard.
1012         // This should be impossible in RESULT state.
1013         // If there is an in-progress explicit evaluation, just cancel it and return.
1014         if (cancelIfEvaluating(false)) return;
1015         setState(CalculatorState.INPUT);
1016         if (haveUnprocessed()) {
1017             mUnprocessedChars = mUnprocessedChars.substring(0, mUnprocessedChars.length() - 1);
1018         } else {
1019             mEvaluator.delete();
1020         }
1021         if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).isEmpty() && !haveUnprocessed()) {
1022             // Resulting formula won't be announced, since it's empty.
1023             announceClearedForAccessibility();
1024         }
1025         redisplayAfterFormulaChange();
1026     }
1027 
reveal(View sourceView, int colorRes, AnimatorListener listener)1028     private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
1029         final ViewGroupOverlay groupOverlay =
1030                 (ViewGroupOverlay) getWindow().getDecorView().getOverlay();
1031 
1032         final Rect displayRect = new Rect();
1033         mDisplayView.getGlobalVisibleRect(displayRect);
1034 
1035         // Make reveal cover the display and status bar.
1036         final View revealView = new View(this);
1037         revealView.setBottom(displayRect.bottom);
1038         revealView.setLeft(displayRect.left);
1039         revealView.setRight(displayRect.right);
1040         revealView.setBackgroundColor(ContextCompat.getColor(this, colorRes));
1041         groupOverlay.add(revealView);
1042 
1043         final int[] clearLocation = new int[2];
1044         sourceView.getLocationInWindow(clearLocation);
1045         clearLocation[0] += sourceView.getWidth() / 2;
1046         clearLocation[1] += sourceView.getHeight() / 2;
1047 
1048         final int revealCenterX = clearLocation[0] - revealView.getLeft();
1049         final int revealCenterY = clearLocation[1] - revealView.getTop();
1050 
1051         final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2);
1052         final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2);
1053         final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2);
1054         final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2));
1055 
1056         final Animator revealAnimator =
1057                 ViewAnimationUtils.createCircularReveal(revealView,
1058                         revealCenterX, revealCenterY, 0.0f, revealRadius);
1059         revealAnimator.setDuration(
1060                 getResources().getInteger(android.R.integer.config_longAnimTime));
1061         revealAnimator.addListener(listener);
1062 
1063         final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
1064         alphaAnimator.setDuration(
1065                 getResources().getInteger(android.R.integer.config_mediumAnimTime));
1066 
1067         final AnimatorSet animatorSet = new AnimatorSet();
1068         animatorSet.play(revealAnimator).before(alphaAnimator);
1069         animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
1070         animatorSet.addListener(new AnimatorListenerAdapter() {
1071             @Override
1072             public void onAnimationEnd(Animator animator) {
1073                 groupOverlay.remove(revealView);
1074                 mCurrentAnimator = null;
1075             }
1076         });
1077 
1078         mCurrentAnimator = animatorSet;
1079         animatorSet.start();
1080     }
1081 
announceClearedForAccessibility()1082     private void announceClearedForAccessibility() {
1083         mResultText.announceForAccessibility(getResources().getString(R.string.cleared));
1084     }
1085 
onClearAnimationEnd()1086     public void onClearAnimationEnd() {
1087          mUnprocessedChars = null;
1088          mResultText.clear();
1089          mEvaluator.clearMain();
1090          setState(CalculatorState.INPUT);
1091          redisplayFormula();
1092     }
1093 
onClear()1094     private void onClear() {
1095         if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).isEmpty() && !haveUnprocessed()) {
1096             return;
1097         }
1098         cancelIfEvaluating(true);
1099         announceClearedForAccessibility();
1100         reveal(mCurrentButton, R.color.calculator_primary_color, new AnimatorListenerAdapter() {
1101             @Override
1102             public void onAnimationEnd(Animator animation) {
1103                 onClearAnimationEnd();
1104                 showOrHideToolbar();
1105             }
1106         });
1107     }
1108 
1109     // Evaluation encountered en error.  Display the error.
1110     @Override
onError(final long index, final int errorResourceId)1111     public void onError(final long index, final int errorResourceId) {
1112         if (index != Evaluator.MAIN_INDEX) {
1113             throw new AssertionError("Unexpected error source");
1114         }
1115         if (mCurrentState == CalculatorState.EVALUATE) {
1116             setState(CalculatorState.ANIMATE);
1117             mResultText.announceForAccessibility(getResources().getString(errorResourceId));
1118             reveal(mCurrentButton, R.color.calculator_error_color,
1119                     new AnimatorListenerAdapter() {
1120                         @Override
1121                         public void onAnimationEnd(Animator animation) {
1122                            setState(CalculatorState.ERROR);
1123                            mResultText.onError(index, errorResourceId);
1124                         }
1125                     });
1126         } else if (mCurrentState == CalculatorState.INIT
1127                 || mCurrentState == CalculatorState.INIT_FOR_RESULT /* very unlikely */) {
1128             setState(CalculatorState.ERROR);
1129             mResultText.onError(index, errorResourceId);
1130         } else {
1131             mResultText.clear();
1132         }
1133     }
1134 
1135     // Animate movement of result into the top formula slot.
1136     // Result window now remains translated in the top slot while the result is displayed.
1137     // (We convert it back to formula use only when the user provides new input.)
1138     // Historical note: In the Lollipop version, this invisibly and instantaneously moved
1139     // formula and result displays back at the end of the animation.  We no longer do that,
1140     // so that we can continue to properly support scrolling of the result.
1141     // We assume the result already contains the text to be expanded.
onResult(boolean animate, boolean resultWasPreserved)1142     private void onResult(boolean animate, boolean resultWasPreserved) {
1143         // Calculate the textSize that would be used to display the result in the formula.
1144         // For scrollable results just use the minimum textSize to maximize the number of digits
1145         // that are visible on screen.
1146         float textSize = mFormulaText.getMinimumTextSize();
1147         if (!mResultText.isScrollable()) {
1148             textSize = mFormulaText.getVariableTextSize(mResultText.getText().toString());
1149         }
1150 
1151         // Scale the result to match the calculated textSize, minimizing the jump-cut transition
1152         // when a result is reused in a subsequent expression.
1153         final float resultScale = textSize / mResultText.getTextSize();
1154 
1155         // Set the result's pivot to match its gravity.
1156         mResultText.setPivotX(mResultText.getWidth() - mResultText.getPaddingRight());
1157         mResultText.setPivotY(mResultText.getHeight() - mResultText.getPaddingBottom());
1158 
1159         // Calculate the necessary translations so the result takes the place of the formula and
1160         // the formula moves off the top of the screen.
1161         final float resultTranslationY = (mFormulaContainer.getBottom() - mResultText.getBottom())
1162                 - (mFormulaText.getPaddingBottom() - mResultText.getPaddingBottom());
1163         float formulaTranslationY = -mFormulaContainer.getBottom();
1164         if (mIsOneLine) {
1165             // Position the result text.
1166             mResultText.setY(mResultText.getBottom());
1167             formulaTranslationY = -(findViewById(R.id.toolbar).getBottom()
1168                     + mFormulaContainer.getBottom());
1169         }
1170 
1171         // Change the result's textColor to match the formula.
1172         final int formulaTextColor = mFormulaText.getCurrentTextColor();
1173 
1174         if (resultWasPreserved) {
1175             // Result was previously addded to history.
1176             mEvaluator.represerve();
1177         } else {
1178             // Add current result to history.
1179             mEvaluator.preserve(Evaluator.MAIN_INDEX, true);
1180         }
1181 
1182         if (animate) {
1183             mResultText.announceForAccessibility(getResources().getString(R.string.desc_eq));
1184             mResultText.announceForAccessibility(mResultText.getText());
1185             setState(CalculatorState.ANIMATE);
1186             final AnimatorSet animatorSet = new AnimatorSet();
1187             animatorSet.playTogether(
1188                     ObjectAnimator.ofPropertyValuesHolder(mResultText,
1189                             PropertyValuesHolder.ofFloat(View.SCALE_X, resultScale),
1190                             PropertyValuesHolder.ofFloat(View.SCALE_Y, resultScale),
1191                             PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, resultTranslationY)),
1192                     ObjectAnimator.ofArgb(mResultText, TEXT_COLOR, formulaTextColor),
1193                     ObjectAnimator.ofFloat(mFormulaContainer, View.TRANSLATION_Y,
1194                             formulaTranslationY));
1195             animatorSet.setDuration(getResources().getInteger(
1196                     android.R.integer.config_longAnimTime));
1197             animatorSet.addListener(new AnimatorListenerAdapter() {
1198                 @Override
1199                 public void onAnimationEnd(Animator animation) {
1200                     setState(CalculatorState.RESULT);
1201                     mCurrentAnimator = null;
1202                 }
1203             });
1204 
1205             mCurrentAnimator = animatorSet;
1206             animatorSet.start();
1207         } else /* No animation desired; get there fast when restarting */ {
1208             mResultText.setScaleX(resultScale);
1209             mResultText.setScaleY(resultScale);
1210             mResultText.setTranslationY(resultTranslationY);
1211             mResultText.setTextColor(formulaTextColor);
1212             mFormulaContainer.setTranslationY(formulaTranslationY);
1213             setState(CalculatorState.RESULT);
1214         }
1215     }
1216 
1217     // Restore positions of the formula and result displays back to their original,
1218     // pre-animation state.
restoreDisplayPositions()1219     private void restoreDisplayPositions() {
1220         // Clear result.
1221         mResultText.setText("");
1222         // Reset all of the values modified during the animation.
1223         mResultText.setScaleX(1.0f);
1224         mResultText.setScaleY(1.0f);
1225         mResultText.setTranslationX(0.0f);
1226         mResultText.setTranslationY(0.0f);
1227         mFormulaContainer.setTranslationY(0.0f);
1228 
1229         mFormulaText.requestFocus();
1230     }
1231 
1232     @Override
onClick(AlertDialogFragment fragment, int which)1233     public void onClick(AlertDialogFragment fragment, int which) {
1234         if (which == DialogInterface.BUTTON_POSITIVE) {
1235             if (HistoryFragment.CLEAR_DIALOG_TAG.equals(fragment.getTag())) {
1236                 // TODO: Try to preserve the current, saved, and memory expressions. How should we
1237                 // handle expressions to which they refer?
1238                 mEvaluator.clearEverything();
1239                 // TODO: It's not clear what we should really do here. This is an initial hack.
1240                 // May want to make onClearAnimationEnd() private if/when we fix this.
1241                 onClearAnimationEnd();
1242                 mEvaluatorCallback.onMemoryStateChanged();
1243                 onBackPressed();
1244             } else if (Evaluator.TIMEOUT_DIALOG_TAG.equals(fragment.getTag())) {
1245                 // Timeout extension request.
1246                 mEvaluator.setLongTimeout();
1247             } else {
1248                 Log.e(TAG, "Unknown AlertDialogFragment click:" + fragment.getTag());
1249             }
1250         }
1251     }
1252 
1253     @Override
onCreateOptionsMenu(Menu menu)1254     public boolean onCreateOptionsMenu(Menu menu) {
1255         super.onCreateOptionsMenu(menu);
1256 
1257         getMenuInflater().inflate(R.menu.activity_calculator, menu);
1258         return true;
1259     }
1260 
1261     @Override
onPrepareOptionsMenu(Menu menu)1262     public boolean onPrepareOptionsMenu(Menu menu) {
1263         super.onPrepareOptionsMenu(menu);
1264 
1265         // Show the leading option when displaying a result.
1266         menu.findItem(R.id.menu_leading).setVisible(mCurrentState == CalculatorState.RESULT);
1267 
1268         // Show the fraction option when displaying a rational result.
1269         boolean visible = mCurrentState == CalculatorState.RESULT;
1270         final UnifiedReal mainResult = mEvaluator.getResult(Evaluator.MAIN_INDEX);
1271         // mainResult should never be null, but it happens. Check as a workaround to protect
1272         // against crashes until we find the root cause (b/34763650).
1273         visible &= mainResult != null && mainResult.exactlyDisplayable();
1274         menu.findItem(R.id.menu_fraction).setVisible(visible);
1275 
1276         return true;
1277     }
1278 
1279     @Override
onOptionsItemSelected(MenuItem item)1280     public boolean onOptionsItemSelected(MenuItem item) {
1281         switch (item.getItemId()) {
1282             case R.id.menu_history:
1283                 showHistoryFragment();
1284                 return true;
1285             case R.id.menu_leading:
1286                 displayFull();
1287                 return true;
1288             case R.id.menu_fraction:
1289                 displayFraction();
1290                 return true;
1291             case R.id.menu_licenses:
1292                 startActivity(new Intent(this, Licenses.class));
1293                 return true;
1294             default:
1295                 return super.onOptionsItemSelected(item);
1296         }
1297     }
1298 
1299     /* Begin override CloseCallback method. */
1300 
1301     @Override
onClose()1302     public void onClose() {
1303         removeHistoryFragment();
1304     }
1305 
1306     /* End override CloseCallback method. */
1307 
1308     /* Begin override DragCallback methods */
1309 
onStartDraggingOpen()1310     public void onStartDraggingOpen() {
1311         mDisplayView.hideToolbar();
1312         showHistoryFragment();
1313     }
1314 
1315     @Override
onInstanceStateRestored(boolean isOpen)1316     public void onInstanceStateRestored(boolean isOpen) {
1317     }
1318 
1319     @Override
whileDragging(float yFraction)1320     public void whileDragging(float yFraction) {
1321     }
1322 
1323     @Override
shouldCaptureView(View view, int x, int y)1324     public boolean shouldCaptureView(View view, int x, int y) {
1325         return view.getId() == R.id.history_frame
1326             && (mDragLayout.isMoving() || mDragLayout.isViewUnder(view, x, y));
1327     }
1328 
1329     @Override
getDisplayHeight()1330     public int getDisplayHeight() {
1331         return mDisplayView.getMeasuredHeight();
1332     }
1333 
1334     /* End override DragCallback methods */
1335 
1336     /**
1337      * Change evaluation state to one that's friendly to the history fragment.
1338      * Return false if that was not easily possible.
1339      */
prepareForHistory()1340     private boolean prepareForHistory() {
1341         if (mCurrentState == CalculatorState.ANIMATE) {
1342             throw new AssertionError("onUserInteraction should have ended animation");
1343         } else if (mCurrentState == CalculatorState.EVALUATE) {
1344             // Cancel current evaluation
1345             cancelIfEvaluating(true /* quiet */ );
1346             setState(CalculatorState.INPUT);
1347             return true;
1348         } else if (mCurrentState == CalculatorState.INIT) {
1349             // Easiest to just refuse.  Otherwise we can see a state change
1350             // while in history mode, which causes all sorts of problems.
1351             // TODO: Consider other alternatives. If we're just doing the decimal conversion
1352             // at the end of an evaluation, we could treat this as RESULT state.
1353             return false;
1354         }
1355         // We should be in INPUT, INIT_FOR_RESULT, RESULT, or ERROR state.
1356         return true;
1357     }
1358 
getHistoryFragment()1359     private HistoryFragment getHistoryFragment() {
1360         final FragmentManager manager = getFragmentManager();
1361         if (manager == null || manager.isDestroyed()) {
1362             return null;
1363         }
1364         final Fragment fragment = manager.findFragmentByTag(HistoryFragment.TAG);
1365         return fragment == null || fragment.isRemoving() ? null : (HistoryFragment) fragment;
1366     }
1367 
showHistoryFragment()1368     private void showHistoryFragment() {
1369         final FragmentManager manager = getFragmentManager();
1370         if (manager == null || manager.isDestroyed()) {
1371             return;
1372         }
1373 
1374         if (getHistoryFragment() != null || !prepareForHistory()) {
1375             return;
1376         }
1377 
1378         stopActionModeOrContextMenu();
1379         manager.beginTransaction()
1380                 .replace(R.id.history_frame, new HistoryFragment(), HistoryFragment.TAG)
1381                 .setTransition(FragmentTransaction.TRANSIT_NONE)
1382                 .addToBackStack(HistoryFragment.TAG)
1383                 .commit();
1384 
1385         // When HistoryFragment is visible, hide all descendants of the main Calculator view.
1386         mMainCalculator.setImportantForAccessibility(
1387                 View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
1388         // TODO: pass current scroll position of result
1389     }
1390 
displayMessage(String title, String message)1391     private void displayMessage(String title, String message) {
1392         AlertDialogFragment.showMessageDialog(this, title, message, null, null /* tag */);
1393     }
1394 
displayFraction()1395     private void displayFraction() {
1396         UnifiedReal result = mEvaluator.getResult(Evaluator.MAIN_INDEX);
1397         displayMessage(getString(R.string.menu_fraction),
1398                 KeyMaps.translateResult(result.toNiceString()));
1399     }
1400 
1401     // Display full result to currently evaluated precision
displayFull()1402     private void displayFull() {
1403         Resources res = getResources();
1404         String msg = mResultText.getFullText(true /* withSeparators */) + " ";
1405         if (mResultText.fullTextIsExact()) {
1406             msg += res.getString(R.string.exact);
1407         } else {
1408             msg += res.getString(R.string.approximate);
1409         }
1410         displayMessage(getString(R.string.menu_leading), msg);
1411     }
1412 
1413     /**
1414      * Add input characters to the end of the expression.
1415      * Map them to the appropriate button pushes when possible.  Leftover characters
1416      * are added to mUnprocessedChars, which is presumed to immediately precede the newly
1417      * added characters.
1418      * @param moreChars characters to be added
1419      * @param explicit these characters were explicitly typed by the user, not pasted
1420      */
addChars(String moreChars, boolean explicit)1421     private void addChars(String moreChars, boolean explicit) {
1422         if (mUnprocessedChars != null) {
1423             moreChars = mUnprocessedChars + moreChars;
1424         }
1425         int current = 0;
1426         int len = moreChars.length();
1427         boolean lastWasDigit = false;
1428         if (mCurrentState == CalculatorState.RESULT && len != 0) {
1429             // Clear display immediately for incomplete function name.
1430             switchToInput(KeyMaps.keyForChar(moreChars.charAt(current)));
1431         }
1432         char groupingSeparator = KeyMaps.translateResult(",").charAt(0);
1433         while (current < len) {
1434             char c = moreChars.charAt(current);
1435             if (Character.isSpaceChar(c) || c == groupingSeparator) {
1436                 ++current;
1437                 continue;
1438             }
1439             int k = KeyMaps.keyForChar(c);
1440             if (!explicit) {
1441                 int expEnd;
1442                 if (lastWasDigit && current !=
1443                         (expEnd = Evaluator.exponentEnd(moreChars, current))) {
1444                     // Process scientific notation with 'E' when pasting, in spite of ambiguity
1445                     // with base of natural log.
1446                     // Otherwise the 10^x key is the user's friend.
1447                     mEvaluator.addExponent(moreChars, current, expEnd);
1448                     current = expEnd;
1449                     lastWasDigit = false;
1450                     continue;
1451                 } else {
1452                     boolean isDigit = KeyMaps.digVal(k) != KeyMaps.NOT_DIGIT;
1453                     if (current == 0 && (isDigit || k == R.id.dec_point)
1454                             && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasTrailingConstant()) {
1455                         // Refuse to concatenate pasted content to trailing constant.
1456                         // This makes pasting of calculator results more consistent, whether or
1457                         // not the old calculator instance is still around.
1458                         addKeyToExpr(R.id.op_mul);
1459                     }
1460                     lastWasDigit = (isDigit || lastWasDigit && k == R.id.dec_point);
1461                 }
1462             }
1463             if (k != View.NO_ID) {
1464                 mCurrentButton = findViewById(k);
1465                 if (explicit) {
1466                     addExplicitKeyToExpr(k);
1467                 } else {
1468                     addKeyToExpr(k);
1469                 }
1470                 if (Character.isSurrogate(c)) {
1471                     current += 2;
1472                 } else {
1473                     ++current;
1474                 }
1475                 continue;
1476             }
1477             int f = KeyMaps.funForString(moreChars, current);
1478             if (f != View.NO_ID) {
1479                 mCurrentButton = findViewById(f);
1480                 if (explicit) {
1481                     addExplicitKeyToExpr(f);
1482                 } else {
1483                     addKeyToExpr(f);
1484                 }
1485                 if (f == R.id.op_sqrt) {
1486                     // Square root entered as function; don't lose the parenthesis.
1487                     addKeyToExpr(R.id.lparen);
1488                 }
1489                 current = moreChars.indexOf('(', current) + 1;
1490                 continue;
1491             }
1492             // There are characters left, but we can't convert them to button presses.
1493             mUnprocessedChars = moreChars.substring(current);
1494             redisplayAfterFormulaChange();
1495             showOrHideToolbar();
1496             return;
1497         }
1498         mUnprocessedChars = null;
1499         redisplayAfterFormulaChange();
1500         showOrHideToolbar();
1501     }
1502 
clearIfNotInputState()1503     private void clearIfNotInputState() {
1504         if (mCurrentState == CalculatorState.ERROR
1505                 || mCurrentState == CalculatorState.RESULT) {
1506             setState(CalculatorState.INPUT);
1507             mEvaluator.clearMain();
1508         }
1509     }
1510 
1511     /**
1512      * Since we only support LTR format, using the RTL comma does not make sense.
1513      */
getDecimalSeparator()1514     private String getDecimalSeparator() {
1515         final char defaultSeparator = DecimalFormatSymbols.getInstance().getDecimalSeparator();
1516         final char rtlComma = '\u066b';
1517         return defaultSeparator == rtlComma ? "," : String.valueOf(defaultSeparator);
1518     }
1519 
1520     /**
1521      * Clean up animation for context menu.
1522      */
1523     @Override
onContextMenuClosed(Menu menu)1524     public void onContextMenuClosed(Menu menu) {
1525         stopActionModeOrContextMenu();
1526     }
1527 
1528     public interface OnDisplayMemoryOperationsListener {
shouldDisplayMemory()1529         boolean shouldDisplayMemory();
1530     }
1531 }
1532