• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 androidx.annotation.NonNull;
48 import androidx.annotation.StringRes;
49 import androidx.core.content.ContextCompat;
50 import androidx.viewpager.widget.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         // Note that ERROR has INPUT, not RESULT layout.
539         return mCurrentState == CalculatorState.INIT_FOR_RESULT
540                 || mCurrentState == CalculatorState.RESULT;
541     }
542 
isOneLine()543     public boolean isOneLine() {
544         return mIsOneLine;
545     }
546 
547     @Override
onDestroy()548     protected void onDestroy() {
549         mDragLayout.removeDragCallback(this);
550         super.onDestroy();
551     }
552 
553     /**
554      * Destroy the evaluator and close the underlying database.
555      */
destroyEvaluator()556     public void destroyEvaluator() {
557         mEvaluator.destroyEvaluator();
558     }
559 
560     @Override
onActionModeStarted(ActionMode mode)561     public void onActionModeStarted(ActionMode mode) {
562         super.onActionModeStarted(mode);
563         if (mode.getTag() == CalculatorFormula.TAG_ACTION_MODE) {
564             mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
565         }
566     }
567 
568     /**
569      * Stop any active ActionMode or ContextMenu for copy/paste actions.
570      * Return true if there was one.
571      */
stopActionModeOrContextMenu()572     private boolean stopActionModeOrContextMenu() {
573         return mResultText.stopActionModeOrContextMenu()
574                 || mFormulaText.stopActionModeOrContextMenu();
575     }
576 
577     @Override
onUserInteraction()578     public void onUserInteraction() {
579         super.onUserInteraction();
580 
581         // If there's an animation in progress, end it immediately, so the user interaction can
582         // be handled.
583         if (mCurrentAnimator != null) {
584             mCurrentAnimator.end();
585         }
586     }
587 
588     @Override
dispatchTouchEvent(MotionEvent e)589     public boolean dispatchTouchEvent(MotionEvent e) {
590         if (e.getActionMasked() == MotionEvent.ACTION_DOWN) {
591             stopActionModeOrContextMenu();
592 
593             final HistoryFragment historyFragment = getHistoryFragment();
594             if (mDragLayout.isOpen() && historyFragment != null) {
595                 historyFragment.stopActionModeOrContextMenu();
596             }
597         }
598         return super.dispatchTouchEvent(e);
599     }
600 
601     @Override
onBackPressed()602     public void onBackPressed() {
603         if (!stopActionModeOrContextMenu()) {
604             final HistoryFragment historyFragment = getHistoryFragment();
605             if (mDragLayout.isOpen() && historyFragment != null) {
606                 if (!historyFragment.stopActionModeOrContextMenu()) {
607                     removeHistoryFragment();
608                 }
609                 return;
610             }
611             if (mPadViewPager != null && mPadViewPager.getCurrentItem() != 0) {
612                 // Select the previous pad.
613                 mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
614             } else {
615                 // If the user is currently looking at the first pad (or the pad is not paged),
616                 // allow the system to handle the Back button.
617                 super.onBackPressed();
618             }
619         }
620     }
621 
622     @Override
onKeyUp(int keyCode, KeyEvent event)623     public boolean onKeyUp(int keyCode, KeyEvent event) {
624         // Allow the system to handle special key codes (e.g. "BACK" or "DPAD").
625         switch (keyCode) {
626             case KeyEvent.KEYCODE_BACK:
627             case KeyEvent.KEYCODE_ESCAPE:
628             case KeyEvent.KEYCODE_DPAD_UP:
629             case KeyEvent.KEYCODE_DPAD_DOWN:
630             case KeyEvent.KEYCODE_DPAD_LEFT:
631             case KeyEvent.KEYCODE_DPAD_RIGHT:
632                 return super.onKeyUp(keyCode, event);
633         }
634 
635         // Stop the action mode or context menu if it's showing.
636         stopActionModeOrContextMenu();
637 
638         // Always cancel unrequested in-progress evaluation of the main expression, so that
639         // we don't have to worry about subsequent asynchronous completion.
640         // Requested in-progress evaluations are handled below.
641         cancelUnrequested();
642 
643         switch (keyCode) {
644             case KeyEvent.KEYCODE_NUMPAD_ENTER:
645             case KeyEvent.KEYCODE_ENTER:
646             case KeyEvent.KEYCODE_DPAD_CENTER:
647                 mCurrentButton = mEqualButton;
648                 onEquals();
649                 return true;
650             case KeyEvent.KEYCODE_DEL:
651                 mCurrentButton = mDeleteButton;
652                 onDelete();
653                 return true;
654             case KeyEvent.KEYCODE_CLEAR:
655                 mCurrentButton = mClearButton;
656                 onClear();
657                 return true;
658             default:
659                 cancelIfEvaluating(false);
660                 final int raw = event.getKeyCharacterMap().get(keyCode, event.getMetaState());
661                 if ((raw & KeyCharacterMap.COMBINING_ACCENT) != 0) {
662                     return true; // discard
663                 }
664                 // Try to discard non-printing characters and the like.
665                 // The user will have to explicitly delete other junk that gets past us.
666                 if (Character.isIdentifierIgnorable(raw) || Character.isWhitespace(raw)) {
667                     return true;
668                 }
669                 char c = (char) raw;
670                 if (c == '=') {
671                     mCurrentButton = mEqualButton;
672                     onEquals();
673                 } else {
674                     addChars(String.valueOf(c), true);
675                     redisplayAfterFormulaChange();
676                 }
677                 return true;
678         }
679     }
680 
681     /**
682      * Invoked whenever the inverse button is toggled to update the UI.
683      *
684      * @param showInverse {@code true} if inverse functions should be shown
685      */
onInverseToggled(boolean showInverse)686     private void onInverseToggled(boolean showInverse) {
687         mInverseToggle.setSelected(showInverse);
688         if (showInverse) {
689             mInverseToggle.setContentDescription(getString(R.string.desc_inv_on));
690             for (View invertibleButton : mInvertibleButtons) {
691                 invertibleButton.setVisibility(View.GONE);
692             }
693             for (View inverseButton : mInverseButtons) {
694                 inverseButton.setVisibility(View.VISIBLE);
695             }
696         } else {
697             mInverseToggle.setContentDescription(getString(R.string.desc_inv_off));
698             for (View invertibleButton : mInvertibleButtons) {
699                 invertibleButton.setVisibility(View.VISIBLE);
700             }
701             for (View inverseButton : mInverseButtons) {
702                 inverseButton.setVisibility(View.GONE);
703             }
704         }
705     }
706 
707     /**
708      * Invoked whenever the deg/rad mode may have changed to update the UI. Note that the mode has
709      * not necessarily actually changed where this is invoked.
710      *
711      * @param degreeMode {@code true} if in degree mode
712      */
onModeChanged(boolean degreeMode)713     private void onModeChanged(boolean degreeMode) {
714         if (degreeMode) {
715             mModeView.setText(R.string.mode_deg);
716             mModeView.setContentDescription(getString(R.string.desc_mode_deg));
717 
718             mModeToggle.setText(R.string.mode_rad);
719             mModeToggle.setContentDescription(getString(R.string.desc_switch_rad));
720         } else {
721             mModeView.setText(R.string.mode_rad);
722             mModeView.setContentDescription(getString(R.string.desc_mode_rad));
723 
724             mModeToggle.setText(R.string.mode_deg);
725             mModeToggle.setContentDescription(getString(R.string.desc_switch_deg));
726         }
727     }
728 
removeHistoryFragment()729     private void removeHistoryFragment() {
730         final FragmentManager manager = getFragmentManager();
731         if (manager != null && !manager.isDestroyed()) {
732             manager.popBackStack(HistoryFragment.TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE);
733         }
734 
735         // When HistoryFragment is hidden, the main Calculator is important for accessibility again.
736         mMainCalculator.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
737     }
738 
739     /**
740      * Switch to INPUT from RESULT state in response to input of the specified button_id.
741      * View.NO_ID is treated as an incomplete function id.
742      */
switchToInput(int button_id)743     private void switchToInput(int button_id) {
744         if (KeyMaps.isBinary(button_id) || KeyMaps.isSuffix(button_id)) {
745             mEvaluator.collapse(mEvaluator.getMaxIndex() /* Most recent history entry */);
746         } else {
747             announceClearedForAccessibility();
748             mEvaluator.clearMain();
749         }
750         setState(CalculatorState.INPUT);
751     }
752 
753     // Add the given button id to input expression.
754     // If appropriate, clear the expression before doing so.
addKeyToExpr(int id)755     private void addKeyToExpr(int id) {
756         if (mCurrentState == CalculatorState.ERROR) {
757             setState(CalculatorState.INPUT);
758         } else if (mCurrentState == CalculatorState.RESULT) {
759             switchToInput(id);
760         }
761         if (!mEvaluator.append(id)) {
762             // TODO: Some user visible feedback?
763         }
764     }
765 
766     /**
767      * Add the given button id to input expression, assuming it was explicitly
768      * typed/touched.
769      * We perform slightly more aggressive correction than in pasted expressions.
770      */
addExplicitKeyToExpr(int id)771     private void addExplicitKeyToExpr(int id) {
772         if (mCurrentState == CalculatorState.INPUT && id == R.id.op_sub) {
773             mEvaluator.getExpr(Evaluator.MAIN_INDEX).removeTrailingAdditiveOperators();
774         }
775         addKeyToExpr(id);
776     }
777 
evaluateInstantIfNecessary()778     public void evaluateInstantIfNecessary() {
779         if (mCurrentState == CalculatorState.INPUT
780                 && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
781             mEvaluator.evaluateAndNotify(Evaluator.MAIN_INDEX, this, mResultText);
782         }
783     }
784 
redisplayAfterFormulaChange()785     private void redisplayAfterFormulaChange() {
786         // TODO: Could do this more incrementally.
787         redisplayFormula();
788         setState(CalculatorState.INPUT);
789         mResultText.clear();
790         if (haveUnprocessed()) {
791             // Force reevaluation when text is deleted, even if expression is unchanged.
792             mEvaluator.touch();
793         } else {
794             evaluateInstantIfNecessary();
795         }
796     }
797 
798     /**
799      * Show the toolbar.
800      * Automatically hide it again if it's not relevant to current formula.
801      */
showAndMaybeHideToolbar()802     private void showAndMaybeHideToolbar() {
803         final boolean shouldBeVisible =
804                 mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
805         mDisplayView.showToolbar(!shouldBeVisible);
806     }
807 
808     /**
809      * Display or hide the toolbar depending on calculator state.
810      */
showOrHideToolbar()811     private void showOrHideToolbar() {
812         final boolean shouldBeVisible =
813                 mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
814         if (shouldBeVisible) {
815             mDisplayView.showToolbar(false);
816         } else {
817             mDisplayView.hideToolbar();
818         }
819     }
820 
onButtonClick(View view)821     public void onButtonClick(View view) {
822         // Any animation is ended before we get here.
823         mCurrentButton = view;
824         stopActionModeOrContextMenu();
825 
826         // See onKey above for the rationale behind some of the behavior below:
827         cancelUnrequested();
828 
829         final int id = view.getId();
830         switch (id) {
831             case R.id.eq:
832                 onEquals();
833                 break;
834             case R.id.del:
835                 onDelete();
836                 break;
837             case R.id.clr:
838                 onClear();
839                 return;  // Toolbar visibility adjusted at end of animation.
840             case R.id.toggle_inv:
841                 final boolean selected = !mInverseToggle.isSelected();
842                 mInverseToggle.setSelected(selected);
843                 onInverseToggled(selected);
844                 if (mCurrentState == CalculatorState.RESULT) {
845                     mResultText.redisplay();   // In case we cancelled reevaluation.
846                 }
847                 break;
848             case R.id.toggle_mode:
849                 cancelIfEvaluating(false);
850                 final boolean mode = !mEvaluator.getDegreeMode(Evaluator.MAIN_INDEX);
851                 if (mCurrentState == CalculatorState.RESULT
852                         && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasTrigFuncs()) {
853                     // Capture current result evaluated in old mode.
854                     mEvaluator.collapse(mEvaluator.getMaxIndex());
855                     redisplayFormula();
856                 }
857                 // In input mode, we reinterpret already entered trig functions.
858                 mEvaluator.setDegreeMode(mode);
859                 onModeChanged(mode);
860                 // Show the toolbar to highlight the mode change.
861                 showAndMaybeHideToolbar();
862                 setState(CalculatorState.INPUT);
863                 mResultText.clear();
864                 if (!haveUnprocessed()) {
865                     evaluateInstantIfNecessary();
866                 }
867                 return;
868             default:
869                 cancelIfEvaluating(false);
870                 if (haveUnprocessed()) {
871                     // For consistency, append as uninterpreted characters.
872                     // This may actually be useful for a left parenthesis.
873                     addChars(KeyMaps.toString(this, id), true);
874                 } else {
875                     addExplicitKeyToExpr(id);
876                     redisplayAfterFormulaChange();
877                 }
878                 break;
879         }
880         showOrHideToolbar();
881     }
882 
redisplayFormula()883     void redisplayFormula() {
884         SpannableStringBuilder formula
885                 = mEvaluator.getExpr(Evaluator.MAIN_INDEX).toSpannableStringBuilder(this);
886         if (mUnprocessedChars != null) {
887             // Add and highlight characters we couldn't process.
888             formula.append(mUnprocessedChars, mUnprocessedColorSpan,
889                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
890         }
891         mFormulaText.changeTextTo(formula);
892         mFormulaText.setContentDescription(TextUtils.isEmpty(formula)
893                 ? getString(R.string.desc_formula) : null);
894     }
895 
896     @Override
onLongClick(View view)897     public boolean onLongClick(View view) {
898         mCurrentButton = view;
899 
900         if (view.getId() == R.id.del) {
901             onClear();
902             return true;
903         }
904         return false;
905     }
906 
907     // Initial evaluation completed successfully.  Initiate display.
onEvaluate(long index, int initDisplayPrec, int msd, int leastDigPos, String truncatedWholeNumber)908     public void onEvaluate(long index, int initDisplayPrec, int msd, int leastDigPos,
909             String truncatedWholeNumber) {
910         if (index != Evaluator.MAIN_INDEX) {
911             throw new AssertionError("Unexpected evaluation result index\n");
912         }
913 
914         // Invalidate any options that may depend on the current result.
915         invalidateOptionsMenu();
916 
917         mResultText.onEvaluate(index, initDisplayPrec, msd, leastDigPos, truncatedWholeNumber);
918         if (mCurrentState != CalculatorState.INPUT) {
919             // In EVALUATE, INIT, RESULT, or INIT_FOR_RESULT state.
920             onResult(mCurrentState == CalculatorState.EVALUATE /* animate */,
921                      mCurrentState == CalculatorState.INIT_FOR_RESULT
922                     || mCurrentState == CalculatorState.RESULT /* previously preserved */);
923         }
924     }
925 
926     // Reset state to reflect evaluator cancellation.  Invoked by evaluator.
onCancelled(long index)927     public void onCancelled(long index) {
928         // Index is Evaluator.MAIN_INDEX. We should be in EVALUATE state.
929         setState(CalculatorState.INPUT);
930         mResultText.onCancelled(index);
931     }
932 
933     // Reevaluation completed; ask result to redisplay current value.
onReevaluate(long index)934     public void onReevaluate(long index) {
935         // Index is Evaluator.MAIN_INDEX.
936         mResultText.onReevaluate(index);
937     }
938 
939     @Override
onTextSizeChanged(final TextView textView, float oldSize)940     public void onTextSizeChanged(final TextView textView, float oldSize) {
941         if (mCurrentState != CalculatorState.INPUT) {
942             // Only animate text changes that occur from user input.
943             return;
944         }
945 
946         // Calculate the values needed to perform the scale and translation animations,
947         // maintaining the same apparent baseline for the displayed text.
948         final float textScale = oldSize / textView.getTextSize();
949         final float translationX = (1.0f - textScale) *
950                 (textView.getWidth() / 2.0f - textView.getPaddingEnd());
951         final float translationY = (1.0f - textScale) *
952                 (textView.getHeight() / 2.0f - textView.getPaddingBottom());
953 
954         final AnimatorSet animatorSet = new AnimatorSet();
955         animatorSet.playTogether(
956                 ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f),
957                 ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f),
958                 ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f),
959                 ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f));
960         animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
961         animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
962         animatorSet.start();
963     }
964 
965     /**
966      * Cancel any in-progress explicitly requested evaluations.
967      * @param quiet suppress pop-up message.  Explicit evaluation can change the expression
968                     value, and certainly changes the display, so it seems reasonable to warn.
969      * @return      true if there was such an evaluation
970      */
cancelIfEvaluating(boolean quiet)971     private boolean cancelIfEvaluating(boolean quiet) {
972         if (mCurrentState == CalculatorState.EVALUATE) {
973             mEvaluator.cancel(Evaluator.MAIN_INDEX, quiet);
974             return true;
975         } else {
976             return false;
977         }
978     }
979 
980 
cancelUnrequested()981     private void cancelUnrequested() {
982         if (mCurrentState == CalculatorState.INPUT) {
983             mEvaluator.cancel(Evaluator.MAIN_INDEX, true);
984         }
985     }
986 
haveUnprocessed()987     private boolean haveUnprocessed() {
988         return mUnprocessedChars != null && !mUnprocessedChars.isEmpty();
989     }
990 
onEquals()991     private void onEquals() {
992         // Ignore if in non-INPUT state, or if there are no operators.
993         if (mCurrentState == CalculatorState.INPUT) {
994             if (haveUnprocessed()) {
995                 setState(CalculatorState.EVALUATE);
996                 onError(Evaluator.MAIN_INDEX, R.string.error_syntax);
997             } else if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
998                 setState(CalculatorState.EVALUATE);
999                 mEvaluator.requireResult(Evaluator.MAIN_INDEX, this, mResultText);
1000             }
1001         }
1002     }
1003 
onDelete()1004     private void onDelete() {
1005         // Delete works like backspace; remove the last character or operator from the expression.
1006         // Note that we handle keyboard delete exactly like the delete button.  For
1007         // example the delete button can be used to delete a character from an incomplete
1008         // function name typed on a physical keyboard.
1009         // This should be impossible in RESULT state.
1010         // If there is an in-progress explicit evaluation, just cancel it and return.
1011         if (cancelIfEvaluating(false)) return;
1012         setState(CalculatorState.INPUT);
1013         if (haveUnprocessed()) {
1014             mUnprocessedChars = mUnprocessedChars.substring(0, mUnprocessedChars.length() - 1);
1015         } else {
1016             mEvaluator.delete();
1017         }
1018         if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).isEmpty() && !haveUnprocessed()) {
1019             // Resulting formula won't be announced, since it's empty.
1020             announceClearedForAccessibility();
1021         }
1022         redisplayAfterFormulaChange();
1023     }
1024 
reveal(View sourceView, int colorRes, AnimatorListener listener)1025     private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
1026         final ViewGroupOverlay groupOverlay =
1027                 (ViewGroupOverlay) getWindow().getDecorView().getOverlay();
1028 
1029         final Rect displayRect = new Rect();
1030         mDisplayView.getGlobalVisibleRect(displayRect);
1031 
1032         // Make reveal cover the display and status bar.
1033         final View revealView = new View(this);
1034         revealView.setBottom(displayRect.bottom);
1035         revealView.setLeft(displayRect.left);
1036         revealView.setRight(displayRect.right);
1037         revealView.setBackgroundColor(ContextCompat.getColor(this, colorRes));
1038         groupOverlay.add(revealView);
1039 
1040         final int[] clearLocation = new int[2];
1041         sourceView.getLocationInWindow(clearLocation);
1042         clearLocation[0] += sourceView.getWidth() / 2;
1043         clearLocation[1] += sourceView.getHeight() / 2;
1044 
1045         final int revealCenterX = clearLocation[0] - revealView.getLeft();
1046         final int revealCenterY = clearLocation[1] - revealView.getTop();
1047 
1048         final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2);
1049         final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2);
1050         final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2);
1051         final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2));
1052 
1053         final Animator revealAnimator =
1054                 ViewAnimationUtils.createCircularReveal(revealView,
1055                         revealCenterX, revealCenterY, 0.0f, revealRadius);
1056         revealAnimator.setDuration(
1057                 getResources().getInteger(android.R.integer.config_longAnimTime));
1058         revealAnimator.addListener(listener);
1059 
1060         final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
1061         alphaAnimator.setDuration(
1062                 getResources().getInteger(android.R.integer.config_mediumAnimTime));
1063 
1064         final AnimatorSet animatorSet = new AnimatorSet();
1065         animatorSet.play(revealAnimator).before(alphaAnimator);
1066         animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
1067         animatorSet.addListener(new AnimatorListenerAdapter() {
1068             @Override
1069             public void onAnimationEnd(Animator animator) {
1070                 groupOverlay.remove(revealView);
1071                 mCurrentAnimator = null;
1072             }
1073         });
1074 
1075         mCurrentAnimator = animatorSet;
1076         animatorSet.start();
1077     }
1078 
announceClearedForAccessibility()1079     private void announceClearedForAccessibility() {
1080         mResultText.announceForAccessibility(getResources().getString(R.string.cleared));
1081     }
1082 
onClearAnimationEnd()1083     public void onClearAnimationEnd() {
1084          mUnprocessedChars = null;
1085          mResultText.clear();
1086          mEvaluator.clearMain();
1087          setState(CalculatorState.INPUT);
1088          redisplayFormula();
1089     }
1090 
onClear()1091     private void onClear() {
1092         if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).isEmpty() && !haveUnprocessed()) {
1093             return;
1094         }
1095         cancelIfEvaluating(true);
1096         announceClearedForAccessibility();
1097         reveal(mCurrentButton, R.color.calculator_primary_color, new AnimatorListenerAdapter() {
1098             @Override
1099             public void onAnimationEnd(Animator animation) {
1100                 onClearAnimationEnd();
1101                 showOrHideToolbar();
1102             }
1103         });
1104     }
1105 
1106     // Evaluation encountered en error.  Display the error.
1107     @Override
onError(final long index, final int errorResourceId)1108     public void onError(final long index, final int errorResourceId) {
1109         if (index != Evaluator.MAIN_INDEX) {
1110             throw new AssertionError("Unexpected error source");
1111         }
1112         if (mCurrentState == CalculatorState.EVALUATE) {
1113             setState(CalculatorState.ANIMATE);
1114             mResultText.announceForAccessibility(getResources().getString(errorResourceId));
1115             reveal(mCurrentButton, R.color.calculator_error_color,
1116                     new AnimatorListenerAdapter() {
1117                         @Override
1118                         public void onAnimationEnd(Animator animation) {
1119                            setState(CalculatorState.ERROR);
1120                            mResultText.onError(index, errorResourceId);
1121                         }
1122                     });
1123         } else if (mCurrentState == CalculatorState.INIT
1124                 || mCurrentState == CalculatorState.INIT_FOR_RESULT /* very unlikely */) {
1125             setState(CalculatorState.ERROR);
1126             mResultText.onError(index, errorResourceId);
1127         } else {
1128             mResultText.clear();
1129         }
1130     }
1131 
1132     // Animate movement of result into the top formula slot.
1133     // Result window now remains translated in the top slot while the result is displayed.
1134     // (We convert it back to formula use only when the user provides new input.)
1135     // Historical note: In the Lollipop version, this invisibly and instantaneously moved
1136     // formula and result displays back at the end of the animation.  We no longer do that,
1137     // so that we can continue to properly support scrolling of the result.
1138     // We assume the result already contains the text to be expanded.
onResult(boolean animate, boolean resultWasPreserved)1139     private void onResult(boolean animate, boolean resultWasPreserved) {
1140         // Calculate the textSize that would be used to display the result in the formula.
1141         // For scrollable results just use the minimum textSize to maximize the number of digits
1142         // that are visible on screen.
1143         float textSize = mFormulaText.getMinimumTextSize();
1144         if (!mResultText.isScrollable()) {
1145             textSize = mFormulaText.getVariableTextSize(mResultText.getText().toString());
1146         }
1147 
1148         // Scale the result to match the calculated textSize, minimizing the jump-cut transition
1149         // when a result is reused in a subsequent expression.
1150         final float resultScale = textSize / mResultText.getTextSize();
1151 
1152         // Set the result's pivot to match its gravity.
1153         mResultText.setPivotX(mResultText.getWidth() - mResultText.getPaddingRight());
1154         mResultText.setPivotY(mResultText.getHeight() - mResultText.getPaddingBottom());
1155 
1156         // Calculate the necessary translations so the result takes the place of the formula and
1157         // the formula moves off the top of the screen.
1158         final float resultTranslationY = (mFormulaContainer.getBottom() - mResultText.getBottom())
1159                 - (mFormulaText.getPaddingBottom() - mResultText.getPaddingBottom());
1160         float formulaTranslationY = -mFormulaContainer.getBottom();
1161         if (mIsOneLine) {
1162             // Position the result text.
1163             mResultText.setY(mResultText.getBottom());
1164             formulaTranslationY = -(findViewById(R.id.toolbar).getBottom()
1165                     + mFormulaContainer.getBottom());
1166         }
1167 
1168         // Change the result's textColor to match the formula.
1169         final int formulaTextColor = mFormulaText.getCurrentTextColor();
1170 
1171         if (resultWasPreserved) {
1172             // Result was previously addded to history.
1173             mEvaluator.represerve();
1174         } else {
1175             // Add current result to history.
1176             mEvaluator.preserve(Evaluator.MAIN_INDEX, true);
1177         }
1178 
1179         if (animate) {
1180             mResultText.announceForAccessibility(getResources().getString(R.string.desc_eq));
1181             mResultText.announceForAccessibility(mResultText.getText());
1182             setState(CalculatorState.ANIMATE);
1183             final AnimatorSet animatorSet = new AnimatorSet();
1184             animatorSet.playTogether(
1185                     ObjectAnimator.ofPropertyValuesHolder(mResultText,
1186                             PropertyValuesHolder.ofFloat(View.SCALE_X, resultScale),
1187                             PropertyValuesHolder.ofFloat(View.SCALE_Y, resultScale),
1188                             PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, resultTranslationY)),
1189                     ObjectAnimator.ofArgb(mResultText, TEXT_COLOR, formulaTextColor),
1190                     ObjectAnimator.ofFloat(mFormulaContainer, View.TRANSLATION_Y,
1191                             formulaTranslationY));
1192             animatorSet.setDuration(getResources().getInteger(
1193                     android.R.integer.config_longAnimTime));
1194             animatorSet.addListener(new AnimatorListenerAdapter() {
1195                 @Override
1196                 public void onAnimationEnd(Animator animation) {
1197                     setState(CalculatorState.RESULT);
1198                     mCurrentAnimator = null;
1199                 }
1200             });
1201 
1202             mCurrentAnimator = animatorSet;
1203             animatorSet.start();
1204         } else /* No animation desired; get there fast when restarting */ {
1205             mResultText.setScaleX(resultScale);
1206             mResultText.setScaleY(resultScale);
1207             mResultText.setTranslationY(resultTranslationY);
1208             mResultText.setTextColor(formulaTextColor);
1209             mFormulaContainer.setTranslationY(formulaTranslationY);
1210             setState(CalculatorState.RESULT);
1211         }
1212     }
1213 
1214     // Restore positions of the formula and result displays back to their original,
1215     // pre-animation state.
restoreDisplayPositions()1216     private void restoreDisplayPositions() {
1217         // Clear result.
1218         mResultText.setText("");
1219         // Reset all of the values modified during the animation.
1220         mResultText.setScaleX(1.0f);
1221         mResultText.setScaleY(1.0f);
1222         mResultText.setTranslationX(0.0f);
1223         mResultText.setTranslationY(0.0f);
1224         mFormulaContainer.setTranslationY(0.0f);
1225 
1226         mFormulaText.requestFocus();
1227     }
1228 
1229     @Override
onClick(AlertDialogFragment fragment, int which)1230     public void onClick(AlertDialogFragment fragment, int which) {
1231         if (which == DialogInterface.BUTTON_POSITIVE) {
1232             if (HistoryFragment.CLEAR_DIALOG_TAG.equals(fragment.getTag())) {
1233                 // TODO: Try to preserve the current, saved, and memory expressions. How should we
1234                 // handle expressions to which they refer?
1235                 mEvaluator.clearEverything();
1236                 // TODO: It's not clear what we should really do here. This is an initial hack.
1237                 // May want to make onClearAnimationEnd() private if/when we fix this.
1238                 onClearAnimationEnd();
1239                 mEvaluatorCallback.onMemoryStateChanged();
1240                 onBackPressed();
1241             } else if (Evaluator.TIMEOUT_DIALOG_TAG.equals(fragment.getTag())) {
1242                 // Timeout extension request.
1243                 mEvaluator.setLongTimeout();
1244             } else {
1245                 Log.e(TAG, "Unknown AlertDialogFragment click:" + fragment.getTag());
1246             }
1247         }
1248     }
1249 
1250     @Override
onCreateOptionsMenu(Menu menu)1251     public boolean onCreateOptionsMenu(Menu menu) {
1252         super.onCreateOptionsMenu(menu);
1253 
1254         getMenuInflater().inflate(R.menu.activity_calculator, menu);
1255         return true;
1256     }
1257 
1258     @Override
onPrepareOptionsMenu(Menu menu)1259     public boolean onPrepareOptionsMenu(Menu menu) {
1260         super.onPrepareOptionsMenu(menu);
1261 
1262         // Show the leading option when displaying a result.
1263         menu.findItem(R.id.menu_leading).setVisible(mCurrentState == CalculatorState.RESULT);
1264 
1265         // Show the fraction option when displaying a rational result.
1266         boolean visible = mCurrentState == CalculatorState.RESULT;
1267         final UnifiedReal mainResult = mEvaluator.getResult(Evaluator.MAIN_INDEX);
1268         // mainResult should never be null, but it happens. Check as a workaround to protect
1269         // against crashes until we find the root cause (b/34763650).
1270         visible &= mainResult != null && mainResult.exactlyDisplayable();
1271         menu.findItem(R.id.menu_fraction).setVisible(visible);
1272 
1273         return true;
1274     }
1275 
1276     @Override
onOptionsItemSelected(MenuItem item)1277     public boolean onOptionsItemSelected(MenuItem item) {
1278         switch (item.getItemId()) {
1279             case R.id.menu_history:
1280                 showHistoryFragment();
1281                 return true;
1282             case R.id.menu_leading:
1283                 displayFull();
1284                 return true;
1285             case R.id.menu_fraction:
1286                 displayFraction();
1287                 return true;
1288             case R.id.menu_licenses:
1289                 startActivity(new Intent(this, Licenses.class));
1290                 return true;
1291             default:
1292                 return super.onOptionsItemSelected(item);
1293         }
1294     }
1295 
1296     /* Begin override CloseCallback method. */
1297 
1298     @Override
onClose()1299     public void onClose() {
1300         removeHistoryFragment();
1301     }
1302 
1303     /* End override CloseCallback method. */
1304 
1305     /* Begin override DragCallback methods */
1306 
onStartDraggingOpen()1307     public void onStartDraggingOpen() {
1308         mDisplayView.hideToolbar();
1309         showHistoryFragment();
1310     }
1311 
1312     @Override
onInstanceStateRestored(boolean isOpen)1313     public void onInstanceStateRestored(boolean isOpen) {
1314     }
1315 
1316     @Override
whileDragging(float yFraction)1317     public void whileDragging(float yFraction) {
1318     }
1319 
1320     @Override
shouldCaptureView(View view, int x, int y)1321     public boolean shouldCaptureView(View view, int x, int y) {
1322         return view.getId() == R.id.history_frame
1323             && (mDragLayout.isMoving() || mDragLayout.isViewUnder(view, x, y));
1324     }
1325 
1326     @Override
getDisplayHeight()1327     public int getDisplayHeight() {
1328         return mDisplayView.getMeasuredHeight();
1329     }
1330 
1331     /* End override DragCallback methods */
1332 
1333     /**
1334      * Change evaluation state to one that's friendly to the history fragment.
1335      * Return false if that was not easily possible.
1336      */
prepareForHistory()1337     private boolean prepareForHistory() {
1338         if (mCurrentState == CalculatorState.ANIMATE) {
1339             // End the current animation and signal that preparation has failed.
1340             // onUserInteraction is unreliable and onAnimationEnd() is asynchronous, so we
1341             // aren't guaranteed to be out of the ANIMATE state by the time prepareForHistory is
1342             // called.
1343             if (mCurrentAnimator != null) {
1344                 mCurrentAnimator.end();
1345             }
1346             return false;
1347         } else if (mCurrentState == CalculatorState.EVALUATE) {
1348             // Cancel current evaluation
1349             cancelIfEvaluating(true /* quiet */ );
1350             setState(CalculatorState.INPUT);
1351             return true;
1352         } else if (mCurrentState == CalculatorState.INIT) {
1353             // Easiest to just refuse.  Otherwise we can see a state change
1354             // while in history mode, which causes all sorts of problems.
1355             // TODO: Consider other alternatives. If we're just doing the decimal conversion
1356             // at the end of an evaluation, we could treat this as RESULT state.
1357             return false;
1358         }
1359         // We should be in INPUT, INIT_FOR_RESULT, RESULT, or ERROR state.
1360         return true;
1361     }
1362 
getHistoryFragment()1363     private HistoryFragment getHistoryFragment() {
1364         final FragmentManager manager = getFragmentManager();
1365         if (manager == null || manager.isDestroyed()) {
1366             return null;
1367         }
1368         final Fragment fragment = manager.findFragmentByTag(HistoryFragment.TAG);
1369         return fragment == null || fragment.isRemoving() ? null : (HistoryFragment) fragment;
1370     }
1371 
showHistoryFragment()1372     private void showHistoryFragment() {
1373         if (getHistoryFragment() != null) {
1374             // If the fragment already exists, do nothing.
1375             return;
1376         }
1377 
1378         final FragmentManager manager = getFragmentManager();
1379         if (manager == null || manager.isDestroyed() || !prepareForHistory()) {
1380             // If the history fragment can not be shown, close the draglayout.
1381             mDragLayout.setClosed();
1382             return;
1383         }
1384 
1385         stopActionModeOrContextMenu();
1386         manager.beginTransaction()
1387                 .replace(R.id.history_frame, new HistoryFragment(), HistoryFragment.TAG)
1388                 .setTransition(FragmentTransaction.TRANSIT_NONE)
1389                 .addToBackStack(HistoryFragment.TAG)
1390                 .commit();
1391 
1392         // When HistoryFragment is visible, hide all descendants of the main Calculator view.
1393         mMainCalculator.setImportantForAccessibility(
1394                 View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
1395         // TODO: pass current scroll position of result
1396     }
1397 
displayMessage(String title, String message)1398     private void displayMessage(String title, String message) {
1399         AlertDialogFragment.showMessageDialog(this, title, message, null, null /* tag */);
1400     }
1401 
displayFraction()1402     private void displayFraction() {
1403         UnifiedReal result = mEvaluator.getResult(Evaluator.MAIN_INDEX);
1404         displayMessage(getString(R.string.menu_fraction),
1405                 KeyMaps.translateResult(result.toNiceString()));
1406     }
1407 
1408     // Display full result to currently evaluated precision
displayFull()1409     private void displayFull() {
1410         Resources res = getResources();
1411         String msg = mResultText.getFullText(true /* withSeparators */) + " ";
1412         if (mResultText.fullTextIsExact()) {
1413             msg += res.getString(R.string.exact);
1414         } else {
1415             msg += res.getString(R.string.approximate);
1416         }
1417         displayMessage(getString(R.string.menu_leading), msg);
1418     }
1419 
1420     /**
1421      * Add input characters to the end of the expression.
1422      * Map them to the appropriate button pushes when possible.  Leftover characters
1423      * are added to mUnprocessedChars, which is presumed to immediately precede the newly
1424      * added characters.
1425      * @param moreChars characters to be added
1426      * @param explicit these characters were explicitly typed by the user, not pasted
1427      */
addChars(String moreChars, boolean explicit)1428     private void addChars(String moreChars, boolean explicit) {
1429         if (mUnprocessedChars != null) {
1430             moreChars = mUnprocessedChars + moreChars;
1431         }
1432         int current = 0;
1433         int len = moreChars.length();
1434         boolean lastWasDigit = false;
1435         if (mCurrentState == CalculatorState.RESULT && len != 0) {
1436             // Clear display immediately for incomplete function name.
1437             switchToInput(KeyMaps.keyForChar(moreChars.charAt(current)));
1438         }
1439         char groupingSeparator = KeyMaps.translateResult(",").charAt(0);
1440         while (current < len) {
1441             char c = moreChars.charAt(current);
1442             if (Character.isSpaceChar(c) || c == groupingSeparator) {
1443                 ++current;
1444                 continue;
1445             }
1446             int k = KeyMaps.keyForChar(c);
1447             if (!explicit) {
1448                 int expEnd;
1449                 if (lastWasDigit && current !=
1450                         (expEnd = Evaluator.exponentEnd(moreChars, current))) {
1451                     // Process scientific notation with 'E' when pasting, in spite of ambiguity
1452                     // with base of natural log.
1453                     // Otherwise the 10^x key is the user's friend.
1454                     mEvaluator.addExponent(moreChars, current, expEnd);
1455                     current = expEnd;
1456                     lastWasDigit = false;
1457                     continue;
1458                 } else {
1459                     boolean isDigit = KeyMaps.digVal(k) != KeyMaps.NOT_DIGIT;
1460                     if (current == 0 && (isDigit || k == R.id.dec_point)
1461                             && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasTrailingConstant()) {
1462                         // Refuse to concatenate pasted content to trailing constant.
1463                         // This makes pasting of calculator results more consistent, whether or
1464                         // not the old calculator instance is still around.
1465                         addKeyToExpr(R.id.op_mul);
1466                     }
1467                     lastWasDigit = (isDigit || lastWasDigit && k == R.id.dec_point);
1468                 }
1469             }
1470             if (k != View.NO_ID) {
1471                 mCurrentButton = findViewById(k);
1472                 if (explicit) {
1473                     addExplicitKeyToExpr(k);
1474                 } else {
1475                     addKeyToExpr(k);
1476                 }
1477                 if (Character.isSurrogate(c)) {
1478                     current += 2;
1479                 } else {
1480                     ++current;
1481                 }
1482                 continue;
1483             }
1484             int f = KeyMaps.funForString(moreChars, current);
1485             if (f != View.NO_ID) {
1486                 mCurrentButton = findViewById(f);
1487                 if (explicit) {
1488                     addExplicitKeyToExpr(f);
1489                 } else {
1490                     addKeyToExpr(f);
1491                 }
1492                 if (f == R.id.op_sqrt) {
1493                     // Square root entered as function; don't lose the parenthesis.
1494                     addKeyToExpr(R.id.lparen);
1495                 }
1496                 current = moreChars.indexOf('(', current) + 1;
1497                 continue;
1498             }
1499             // There are characters left, but we can't convert them to button presses.
1500             mUnprocessedChars = moreChars.substring(current);
1501             redisplayAfterFormulaChange();
1502             showOrHideToolbar();
1503             return;
1504         }
1505         mUnprocessedChars = null;
1506         redisplayAfterFormulaChange();
1507         showOrHideToolbar();
1508     }
1509 
clearIfNotInputState()1510     private void clearIfNotInputState() {
1511         if (mCurrentState == CalculatorState.ERROR
1512                 || mCurrentState == CalculatorState.RESULT) {
1513             setState(CalculatorState.INPUT);
1514             mEvaluator.clearMain();
1515         }
1516     }
1517 
1518     /**
1519      * Since we only support LTR format, using the RTL comma does not make sense.
1520      */
getDecimalSeparator()1521     private String getDecimalSeparator() {
1522         final char defaultSeparator = DecimalFormatSymbols.getInstance().getDecimalSeparator();
1523         final char rtlComma = '\u066b';
1524         return defaultSeparator == rtlComma ? "," : String.valueOf(defaultSeparator);
1525     }
1526 
1527     /**
1528      * Clean up animation for context menu.
1529      */
1530     @Override
onContextMenuClosed(Menu menu)1531     public void onContextMenuClosed(Menu menu) {
1532         stopActionModeOrContextMenu();
1533     }
1534 
1535     public interface OnDisplayMemoryOperationsListener {
shouldDisplayMemory()1536         boolean shouldDisplayMemory();
1537     }
1538 }
1539