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
26package com.android.calculator2;
27
28import android.animation.Animator;
29import android.animation.Animator.AnimatorListener;
30import android.animation.AnimatorListenerAdapter;
31import android.animation.AnimatorSet;
32import android.animation.ObjectAnimator;
33import android.animation.PropertyValuesHolder;
34import android.app.ActionBar;
35import android.app.Activity;
36import android.app.Fragment;
37import android.app.FragmentManager;
38import android.app.FragmentTransaction;
39import android.content.ClipData;
40import android.content.DialogInterface;
41import android.content.Intent;
42import android.content.res.Resources;
43import android.graphics.Color;
44import android.graphics.Rect;
45import android.net.Uri;
46import android.os.Bundle;
47import android.support.annotation.NonNull;
48import android.support.annotation.StringRes;
49import android.support.v4.content.ContextCompat;
50import android.support.v4.view.ViewPager;
51import android.text.Editable;
52import android.text.SpannableStringBuilder;
53import android.text.Spanned;
54import android.text.TextUtils;
55import android.text.TextWatcher;
56import android.text.style.ForegroundColorSpan;
57import android.util.Log;
58import android.util.Property;
59import android.view.ActionMode;
60import android.view.KeyCharacterMap;
61import android.view.KeyEvent;
62import android.view.Menu;
63import android.view.MenuItem;
64import android.view.MotionEvent;
65import android.view.View;
66import android.view.View.OnLongClickListener;
67import android.view.ViewAnimationUtils;
68import android.view.ViewGroupOverlay;
69import android.view.ViewTreeObserver;
70import android.view.animation.AccelerateDecelerateInterpolator;
71import android.widget.HorizontalScrollView;
72import android.widget.TextView;
73import android.widget.Toolbar;
74
75import com.android.calculator2.CalculatorFormula.OnTextSizeChangeListener;
76
77import java.io.ByteArrayInputStream;
78import java.io.ByteArrayOutputStream;
79import java.io.IOException;
80import java.io.ObjectInput;
81import java.io.ObjectInputStream;
82import java.io.ObjectOutput;
83import java.io.ObjectOutputStream;
84import java.text.DecimalFormatSymbols;
85
86import static com.android.calculator2.CalculatorFormula.OnFormulaContextMenuClickListener;
87
88public 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     */
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     */
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.
349            setState(mapFromSaved(mCurrentState));
350            // Request evaluation when we know display width.
351            mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_REQUIRE, this);
352        }
353    }
354
355    @Override
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
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
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.
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
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
546    public boolean isOneLine() {
547        return mIsOneLine;
548    }
549
550    @Override
551    protected void onDestroy() {
552        mDragLayout.removeDragCallback(this);
553        super.onDestroy();
554    }
555
556    /**
557     * Destroy the evaluator and close the underlying database.
558     */
559    public void destroyEvaluator() {
560        mEvaluator.destroyEvaluator();
561    }
562
563    @Override
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     */
575    private boolean stopActionModeOrContextMenu() {
576        return mResultText.stopActionModeOrContextMenu()
577                || mFormulaText.stopActionModeOrContextMenu();
578    }
579
580    @Override
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
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
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
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     */
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     */
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
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     */
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.
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     */
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
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
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     */
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     */
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
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
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
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.
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.
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.
937    public void onReevaluate(long index) {
938        // Index is Evaluator.MAIN_INDEX.
939        mResultText.onReevaluate(index);
940    }
941
942    @Override
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     */
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
984    private void cancelUnrequested() {
985        if (mCurrentState == CalculatorState.INPUT) {
986            mEvaluator.cancel(Evaluator.MAIN_INDEX, true);
987        }
988    }
989
990    private boolean haveUnprocessed() {
991        return mUnprocessedChars != null && !mUnprocessedChars.isEmpty();
992    }
993
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
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
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
1082    private void announceClearedForAccessibility() {
1083        mResultText.announceForAccessibility(getResources().getString(R.string.cleared));
1084    }
1085
1086    public void onClearAnimationEnd() {
1087         mUnprocessedChars = null;
1088         mResultText.clear();
1089         mEvaluator.clearMain();
1090         setState(CalculatorState.INPUT);
1091         redisplayFormula();
1092    }
1093
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
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.
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.
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
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
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
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
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
1302    public void onClose() {
1303        removeHistoryFragment();
1304    }
1305
1306    /* End override CloseCallback method. */
1307
1308    /* Begin override DragCallback methods */
1309
1310    public void onStartDraggingOpen() {
1311        mDisplayView.hideToolbar();
1312        showHistoryFragment();
1313    }
1314
1315    @Override
1316    public void onInstanceStateRestored(boolean isOpen) {
1317    }
1318
1319    @Override
1320    public void whileDragging(float yFraction) {
1321    }
1322
1323    @Override
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
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     */
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
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
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
1391    private void displayMessage(String title, String message) {
1392        AlertDialogFragment.showMessageDialog(this, title, message, null, null /* tag */);
1393    }
1394
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
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     */
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
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     */
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
1524    public void onContextMenuClosed(Menu menu) {
1525        stopActionModeOrContextMenu();
1526    }
1527
1528    public interface OnDisplayMemoryOperationsListener {
1529        boolean shouldDisplayMemory();
1530    }
1531}
1532