blob: 9af6bc19f47ab0196149e16de6880c8a07861877 [file] [log] [blame]
Justin Klaassen4b3af052014-05-27 17:53:10 -07001/*
Justin Klaassen12da1ad2016-04-04 14:20:37 -07002 * Copyright (C) 2016 The Android Open Source Project
Justin Klaassen4b3af052014-05-27 17:53:10 -07003 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Hans Boehm013969e2015-04-13 20:29:47 -070017// TODO: Copy & more general paste in formula? Note that this requires
18// great care: Currently the text version of a displayed formula
19// is not directly useful for re-evaluating the formula later, since
20// it contains ellipses representing subexpressions evaluated with
21// a different degree mode. Rather than supporting copy from the
22// formula window, we may eventually want to support generation of a
23// more useful text version in a separate window. It's not clear
24// this is worth the added (code and user) complexity.
Hans Boehm84614952014-11-25 18:46:17 -080025
Justin Klaassen4b3af052014-05-27 17:53:10 -070026package com.android.calculator2;
27
28import android.animation.Animator;
Justin Klaassen5f2a3342014-06-11 17:40:22 -070029import android.animation.Animator.AnimatorListener;
Justin Klaassen4b3af052014-05-27 17:53:10 -070030import android.animation.AnimatorListenerAdapter;
31import android.animation.AnimatorSet;
Justin Klaassen4b3af052014-05-27 17:53:10 -070032import android.animation.ObjectAnimator;
Justin Klaassen44595162015-05-28 17:55:20 -070033import android.animation.PropertyValuesHolder;
Justin Klaassen9d33cdc2016-02-21 14:16:14 -080034import android.app.ActionBar;
Justin Klaassen4b3af052014-05-27 17:53:10 -070035import android.app.Activity;
Annie Chin06fd3cf2016-11-07 16:04:33 -080036import android.app.FragmentManager;
Annie Chin09547532016-10-14 10:59:07 -070037import android.app.FragmentTransaction;
Justin Klaassenfc5ac822015-06-18 13:15:17 -070038import android.content.ClipData;
Hans Boehm5e6a0ca2015-09-22 17:09:01 -070039import android.content.DialogInterface;
Justin Klaassend36d63e2015-05-05 12:59:36 -070040import android.content.Intent;
Hans Boehmbfe8c222015-04-02 16:26:07 -070041import android.content.res.Resources;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070042import android.graphics.Color;
Justin Klaassen8fff1442014-06-19 10:43:29 -070043import android.graphics.Rect;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070044import android.net.Uri;
Justin Klaassen4b3af052014-05-27 17:53:10 -070045import android.os.Bundle;
Justin Klaassenf79d6f62014-08-26 12:27:08 -070046import android.support.annotation.NonNull;
Christine Frankscbc51fa2017-01-04 21:00:36 -080047import android.support.annotation.StringRes;
Chenjie Yu3937b652016-06-01 23:14:26 -070048import android.support.v4.content.ContextCompat;
Justin Klaassen3b4d13d2014-06-06 18:18:37 +010049import android.support.v4.view.ViewPager;
Annie Chine918fd22016-03-09 11:07:54 -080050import android.text.Editable;
Hans Boehm8a4f81c2015-07-09 10:41:25 -070051import android.text.SpannableStringBuilder;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070052import android.text.Spanned;
Annie Chinf360ef02016-03-10 13:45:39 -080053import android.text.TextUtils;
Annie Chine918fd22016-03-09 11:07:54 -080054import android.text.TextWatcher;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070055import android.text.style.ForegroundColorSpan;
Annie Chin532b77e2016-12-06 13:30:35 -080056import android.util.Log;
Justin Klaassen44595162015-05-28 17:55:20 -070057import android.util.Property;
Annie Chine918fd22016-03-09 11:07:54 -080058import android.view.ActionMode;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070059import android.view.KeyCharacterMap;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -070060import android.view.KeyEvent;
Hans Boehm84614952014-11-25 18:46:17 -080061import android.view.Menu;
62import android.view.MenuItem;
Annie Chind0f87d22016-10-24 09:04:12 -070063import android.view.MotionEvent;
Justin Klaassen4b3af052014-05-27 17:53:10 -070064import android.view.View;
65import android.view.View.OnLongClickListener;
Justin Klaassen5f2a3342014-06-11 17:40:22 -070066import android.view.ViewAnimationUtils;
Justin Klaassen8fff1442014-06-19 10:43:29 -070067import android.view.ViewGroupOverlay;
Annie Chine918fd22016-03-09 11:07:54 -080068import android.view.ViewTreeObserver;
Justin Klaassen4b3af052014-05-27 17:53:10 -070069import android.view.animation.AccelerateDecelerateInterpolator;
Annie Chind0f87d22016-10-24 09:04:12 -070070import android.widget.FrameLayout;
Annie Chine918fd22016-03-09 11:07:54 -080071import android.widget.HorizontalScrollView;
Justin Klaassenfed941a2014-06-09 18:42:40 +010072import android.widget.TextView;
Justin Klaassend48b7562015-04-16 16:51:38 -070073import android.widget.Toolbar;
Justin Klaassenfed941a2014-06-09 18:42:40 +010074
Christine Franks7452d3a2016-10-27 13:41:18 -070075import com.android.calculator2.CalculatorFormula.OnTextSizeChangeListener;
Hans Boehm84614952014-11-25 18:46:17 -080076
77import java.io.ByteArrayInputStream;
Hans Boehm84614952014-11-25 18:46:17 -080078import java.io.ByteArrayOutputStream;
Hans Boehm84614952014-11-25 18:46:17 -080079import java.io.IOException;
Justin Klaassen721ec842015-05-28 14:30:08 -070080import java.io.ObjectInput;
81import java.io.ObjectInputStream;
82import java.io.ObjectOutput;
83import java.io.ObjectOutputStream;
Christine Franksbd90b792016-11-22 10:28:26 -080084import java.text.DecimalFormatSymbols;
Justin Klaassen4b3af052014-05-27 17:53:10 -070085
Christine Franks1d99be12016-11-14 14:00:36 -080086import static com.android.calculator2.CalculatorFormula.OnFormulaContextMenuClickListener;
87
Hans Boehm8f051c32016-10-03 16:53:58 -070088public class Calculator extends Activity
Christine Franks1d99be12016-11-14 14:00:36 -080089 implements OnTextSizeChangeListener, OnLongClickListener,
Hans Boehm8f051c32016-10-03 16:53:58 -070090 AlertDialogFragment.OnClickListener, Evaluator.EvaluationListener /* for main result */ {
Justin Klaassen2be4fdb2014-08-06 19:54:09 -070091
Annie Chin9a211132016-11-30 12:52:06 -080092 private static final String TAG = "Calculator";
Justin Klaassen2be4fdb2014-08-06 19:54:09 -070093 /**
94 * Constant for an invalid resource id.
95 */
96 public static final int INVALID_RES_ID = -1;
Justin Klaassen4b3af052014-05-27 17:53:10 -070097
98 private enum CalculatorState {
Hans Boehm84614952014-11-25 18:46:17 -080099 INPUT, // Result and formula both visible, no evaluation requested,
100 // Though result may be visible on bottom line.
101 EVALUATE, // Both visible, evaluation requested, evaluation/animation incomplete.
Hans Boehmc1ea0912015-06-19 15:05:07 -0700102 // Not used for instant result evaluation.
Hans Boehm84614952014-11-25 18:46:17 -0800103 INIT, // Very temporary state used as alternative to EVALUATE
104 // during reinitialization. Do not animate on completion.
Hans Boehma5ea8eb2016-12-01 12:33:38 -0800105 INIT_FOR_RESULT, // Identical to INIT, but evaluation is known to terminate
106 // with result, and current expression has been copied to history.
Hans Boehm84614952014-11-25 18:46:17 -0800107 ANIMATE, // Result computed, animation to enlarge result window in progress.
108 RESULT, // Result displayed, formula invisible.
109 // If we are in RESULT state, the formula was evaluated without
110 // error to initial precision.
Hans Boehm8f051c32016-10-03 16:53:58 -0700111 // The current formula is now also the last history entry.
Hans Boehm84614952014-11-25 18:46:17 -0800112 ERROR // Error displayed: Formula visible, result shows error message.
113 // Display similar to INPUT state.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700114 }
Hans Boehm84614952014-11-25 18:46:17 -0800115 // Normal transition sequence is
116 // INPUT -> EVALUATE -> ANIMATE -> RESULT (or ERROR) -> INPUT
117 // A RESULT -> ERROR transition is possible in rare corner cases, in which
118 // a higher precision evaluation exposes an error. This is possible, since we
119 // initially evaluate assuming we were given a well-defined problem. If we
120 // were actually asked to compute sqrt(<extremely tiny negative number>) we produce 0
121 // unless we are asked for enough precision that we can distinguish the argument from zero.
Hans Boehma5ea8eb2016-12-01 12:33:38 -0800122 // ERROR and RESULT are translated to INIT or INIT_FOR_RESULT state if the application
Hans Boehm84614952014-11-25 18:46:17 -0800123 // is restarted in that state. This leads us to recompute and redisplay the result
Hans Boehma5ea8eb2016-12-01 12:33:38 -0800124 // ASAP. We avoid saving the ANIMATE state or activating history in that state.
125 // In INIT_FOR_RESULT, and RESULT state, a copy of the current
126 // expression has been saved in the history db; in the other non-ANIMATE states,
127 // it has not.
Hans Boehm84614952014-11-25 18:46:17 -0800128 // TODO: Possibly save a bit more information, e.g. its initial display string
129 // or most significant digit position, to speed up restart.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700130
Justin Klaassen44595162015-05-28 17:55:20 -0700131 private final Property<TextView, Integer> TEXT_COLOR =
132 new Property<TextView, Integer>(Integer.class, "textColor") {
133 @Override
134 public Integer get(TextView textView) {
135 return textView.getCurrentTextColor();
136 }
137
138 @Override
139 public void set(TextView textView, Integer textColor) {
140 textView.setTextColor(textColor);
141 }
142 };
143
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800144 private static final String NAME = "Calculator";
Hans Boehm84614952014-11-25 18:46:17 -0800145 private static final String KEY_DISPLAY_STATE = NAME + "_display_state";
Hans Boehm760a9dc2015-04-20 10:27:12 -0700146 private static final String KEY_UNPROCESSED_CHARS = NAME + "_unprocessed_chars";
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800147 /**
148 * Associated value is a byte array holding the evaluator state.
149 */
Hans Boehm84614952014-11-25 18:46:17 -0800150 private static final String KEY_EVAL_STATE = NAME + "_eval_state";
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800151 private static final String KEY_INVERSE_MODE = NAME + "_inverse_mode";
Christine Frankseeff27f2016-07-29 12:05:29 -0700152 /**
153 * Associated value is an boolean holding the visibility state of the toolbar.
154 */
155 private static final String KEY_SHOW_TOOLBAR = NAME + "_show_toolbar";
Justin Klaassen741471e2014-06-11 09:43:44 -0700156
Annie Chine918fd22016-03-09 11:07:54 -0800157 private final ViewTreeObserver.OnPreDrawListener mPreDrawListener =
158 new ViewTreeObserver.OnPreDrawListener() {
159 @Override
160 public boolean onPreDraw() {
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700161 mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
Annie Chine918fd22016-03-09 11:07:54 -0800162 final ViewTreeObserver observer = mFormulaContainer.getViewTreeObserver();
163 if (observer.isAlive()) {
164 observer.removeOnPreDrawListener(this);
165 }
166 return false;
167 }
168 };
169
Christine Frankscbc51fa2017-01-04 21:00:36 -0800170 private final Evaluator.Callback mEvaluatorCallback = new Evaluator.Callback() {
171 @Override
172 public void onMemoryStateChanged() {
173 mFormulaText.onMemoryStateChanged();
174 }
175
176 @Override
177 public void showMessageDialog(@StringRes int title, @StringRes int message,
178 @StringRes int positiveButtonLabel, String tag) {
179 AlertDialogFragment.showMessageDialog(Calculator.this, title, message,
180 positiveButtonLabel, tag);
181
182 }
183 };
184
185 private final OnDisplayMemoryOperationsListener mOnDisplayMemoryOperationsListener =
Christine Franks1d99be12016-11-14 14:00:36 -0800186 new OnDisplayMemoryOperationsListener() {
187 @Override
188 public boolean shouldDisplayMemory() {
189 return mEvaluator.getMemoryIndex() != 0;
190 }
191 };
192
Christine Frankscbc51fa2017-01-04 21:00:36 -0800193 private final OnFormulaContextMenuClickListener mOnFormulaContextMenuClickListener =
Christine Franks1d99be12016-11-14 14:00:36 -0800194 new OnFormulaContextMenuClickListener() {
195 @Override
196 public boolean onPaste(ClipData clip) {
197 final ClipData.Item item = clip.getItemCount() == 0 ? null : clip.getItemAt(0);
198 if (item == null) {
199 // nothing to paste, bail early...
200 return false;
201 }
202
203 // Check if the item is a previously copied result, otherwise paste as raw text.
204 final Uri uri = item.getUri();
205 if (uri != null && mEvaluator.isLastSaved(uri)) {
206 clearIfNotInputState();
207 mEvaluator.appendExpr(mEvaluator.getSavedIndex());
208 redisplayAfterFormulaChange();
209 } else {
210 addChars(item.coerceToText(Calculator.this).toString(), false);
211 }
212 return true;
213 }
214
215 @Override
216 public void onMemoryRecall() {
217 clearIfNotInputState();
218 long memoryIndex = mEvaluator.getMemoryIndex();
219 if (memoryIndex != 0) {
220 mEvaluator.appendExpr(mEvaluator.getMemoryIndex());
221 redisplayAfterFormulaChange();
Hans Boehmcc368502016-12-09 10:44:46 -0800222 }
Christine Franks1d99be12016-11-14 14:00:36 -0800223 }
224 };
225
226
Annie Chine918fd22016-03-09 11:07:54 -0800227 private final TextWatcher mFormulaTextWatcher = new TextWatcher() {
228 @Override
229 public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
230 }
231
232 @Override
233 public void onTextChanged(CharSequence charSequence, int start, int count, int after) {
234 }
235
236 @Override
237 public void afterTextChanged(Editable editable) {
238 final ViewTreeObserver observer = mFormulaContainer.getViewTreeObserver();
239 if (observer.isAlive()) {
240 observer.removeOnPreDrawListener(mPreDrawListener);
241 observer.addOnPreDrawListener(mPreDrawListener);
242 }
243 }
244 };
245
Annie Chin9a211132016-11-30 12:52:06 -0800246 private final DragLayout.CloseCallback mCloseCallback = new DragLayout.CloseCallback() {
247 @Override
248 public void onClose() {
Justin Klaassen12874e32016-12-12 07:57:47 -0800249 removeHistoryFragment();
Annie Chin9a211132016-11-30 12:52:06 -0800250 }
251 };
252
Annie Chind0f87d22016-10-24 09:04:12 -0700253 private final DragLayout.DragCallback mDragCallback = new DragLayout.DragCallback() {
254 @Override
Annie Chin9a211132016-11-30 12:52:06 -0800255 public void onStartDraggingOpen() {
Annie Chind0f87d22016-10-24 09:04:12 -0700256 showHistoryFragment(FragmentTransaction.TRANSIT_NONE);
257 }
258
259 @Override
260 public void whileDragging(float yFraction) {
261 // no-op
262 }
263
264 @Override
Annie Chind3443222016-12-07 17:19:07 -0800265 public boolean shouldCaptureView(View view, int x, int y) {
266 return mDragLayout.isMoving()
267 || mDragLayout.isOpen()
268 || mDragLayout.isViewUnder(mDisplayView, x, y);
Annie Chind0f87d22016-10-24 09:04:12 -0700269 }
270
271 @Override
272 public int getDisplayHeight() {
273 return mDisplayView.getMeasuredHeight();
274 }
275
276 public void onLayout(int translation) {
277 mHistoryFrame.setTranslationY(translation + mDisplayView.getBottom());
278 }
279 };
280
Justin Klaassen4b3af052014-05-27 17:53:10 -0700281 private CalculatorState mCurrentState;
Hans Boehm84614952014-11-25 18:46:17 -0800282 private Evaluator mEvaluator;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700283
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800284 private CalculatorDisplay mDisplayView;
Justin Klaassend48b7562015-04-16 16:51:38 -0700285 private TextView mModeView;
Christine Franks7452d3a2016-10-27 13:41:18 -0700286 private CalculatorFormula mFormulaText;
Justin Klaassen44595162015-05-28 17:55:20 -0700287 private CalculatorResult mResultText;
Annie Chine918fd22016-03-09 11:07:54 -0800288 private HorizontalScrollView mFormulaContainer;
Annie Chin09547532016-10-14 10:59:07 -0700289 private DragLayout mDragLayout;
Annie Chind0f87d22016-10-24 09:04:12 -0700290 private FrameLayout mHistoryFrame;
Justin Klaassend48b7562015-04-16 16:51:38 -0700291
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100292 private ViewPager mPadViewPager;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700293 private View mDeleteButton;
294 private View mClearButton;
Justin Klaassend48b7562015-04-16 16:51:38 -0700295 private View mEqualButton;
Annie Chineb36f952016-12-08 17:27:19 -0800296 private View mMainCalculator;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700297
298 private TextView mInverseToggle;
299 private TextView mModeToggle;
300
Justin Klaassen721ec842015-05-28 14:30:08 -0700301 private View[] mInvertibleButtons;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700302 private View[] mInverseButtons;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700303
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700304 private View mCurrentButton;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700305 private Animator mCurrentAnimator;
306
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700307 // Characters that were recently entered at the end of the display that have not yet
308 // been added to the underlying expression.
309 private String mUnprocessedChars = null;
310
311 // Color to highlight unprocessed characters from physical keyboard.
312 // TODO: should probably match this to the error color?
313 private ForegroundColorSpan mUnprocessedColorSpan = new ForegroundColorSpan(Color.RED);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700314
Annie Chin26e159e2016-05-18 15:17:14 -0700315 // Whether the display is one line.
316 private boolean mOneLine;
317
Annie Chin09547532016-10-14 10:59:07 -0700318 private HistoryFragment mHistoryFragment = new HistoryFragment();
319
Hans Boehm31ea2522016-11-23 17:47:02 -0800320 /**
Hans Boehma5ea8eb2016-12-01 12:33:38 -0800321 * Map the old saved state to a new state reflecting requested result reevaluation.
322 */
323 private CalculatorState mapFromSaved(CalculatorState savedState) {
324 switch (savedState) {
325 case RESULT:
326 case INIT_FOR_RESULT:
327 // Evaluation is expected to terminate normally.
328 return CalculatorState.INIT_FOR_RESULT;
329 case ERROR:
330 case INIT:
331 return CalculatorState.INIT;
332 case EVALUATE:
333 case INPUT:
334 return savedState;
335 default: // Includes ANIMATE state.
336 throw new AssertionError("Impossible saved state");
337 }
338 }
339
340 /**
Hans Boehm31ea2522016-11-23 17:47:02 -0800341 * Restore Evaluator state and mCurrentState from savedInstanceState.
342 * Return true if the toolbar should be visible.
343 */
344 private void restoreInstanceState(Bundle savedInstanceState) {
345 final CalculatorState savedState = CalculatorState.values()[
346 savedInstanceState.getInt(KEY_DISPLAY_STATE,
347 CalculatorState.INPUT.ordinal())];
348 setState(savedState);
349 CharSequence unprocessed = savedInstanceState.getCharSequence(KEY_UNPROCESSED_CHARS);
350 if (unprocessed != null) {
351 mUnprocessedChars = unprocessed.toString();
352 }
353 byte[] state = savedInstanceState.getByteArray(KEY_EVAL_STATE);
354 if (state != null) {
355 try (ObjectInput in = new ObjectInputStream(new ByteArrayInputStream(state))) {
356 mEvaluator.restoreInstanceState(in);
357 } catch (Throwable ignored) {
358 // When in doubt, revert to clean state
359 mCurrentState = CalculatorState.INPUT;
360 mEvaluator.clearMain();
361 }
362 }
363 if (savedInstanceState.getBoolean(KEY_SHOW_TOOLBAR, true)) {
364 showAndMaybeHideToolbar();
365 } else {
366 mDisplayView.hideToolbar();
367 }
368 onInverseToggled(savedInstanceState.getBoolean(KEY_INVERSE_MODE));
369 // TODO: We're currently not saving and restoring scroll position.
370 // We probably should. Details may require care to deal with:
371 // - new display size
372 // - slow recomputation if we've scrolled far.
373 }
374
375 private void restoreDisplay() {
376 onModeChanged(mEvaluator.getDegreeMode(Evaluator.MAIN_INDEX));
377 if (mCurrentState != CalculatorState.RESULT
378 && mCurrentState != CalculatorState.INIT_FOR_RESULT) {
379 redisplayFormula();
380 }
381 if (mCurrentState == CalculatorState.INPUT) {
382 // This resultText will explicitly call evaluateAndNotify when ready.
383 mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_EVALUATE, this);
384 } else {
385 // Just reevaluate.
Hans Boehma5ea8eb2016-12-01 12:33:38 -0800386 setState(mapFromSaved(mCurrentState));
Hans Boehm31ea2522016-11-23 17:47:02 -0800387 // Request evaluation when we know display width.
388 mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_REQUIRE, this);
389 }
390 }
391
Justin Klaassen4b3af052014-05-27 17:53:10 -0700392 @Override
393 protected void onCreate(Bundle savedInstanceState) {
394 super.onCreate(savedInstanceState);
Hans Boehm31ea2522016-11-23 17:47:02 -0800395
Annie Chin09547532016-10-14 10:59:07 -0700396 setContentView(R.layout.activity_calculator_main);
Justin Klaassend48b7562015-04-16 16:51:38 -0700397 setActionBar((Toolbar) findViewById(R.id.toolbar));
398
399 // Hide all default options in the ActionBar.
400 getActionBar().setDisplayOptions(0);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700401
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800402 // Ensure the toolbar stays visible while the options menu is displayed.
403 getActionBar().addOnMenuVisibilityListener(new ActionBar.OnMenuVisibilityListener() {
404 @Override
405 public void onMenuVisibilityChanged(boolean isVisible) {
406 mDisplayView.setForceToolbarVisible(isVisible);
407 }
408 });
409
Annie Chineb36f952016-12-08 17:27:19 -0800410 mMainCalculator = findViewById(R.id.main_calculator);
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800411 mDisplayView = (CalculatorDisplay) findViewById(R.id.display);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700412 mModeView = (TextView) findViewById(R.id.mode);
Christine Franks7452d3a2016-10-27 13:41:18 -0700413 mFormulaText = (CalculatorFormula) findViewById(R.id.formula);
Justin Klaassen44595162015-05-28 17:55:20 -0700414 mResultText = (CalculatorResult) findViewById(R.id.result);
Annie Chine918fd22016-03-09 11:07:54 -0800415 mFormulaContainer = (HorizontalScrollView) findViewById(R.id.formula_container);
Hans Boehm31ea2522016-11-23 17:47:02 -0800416 mEvaluator = Evaluator.getInstance(this);
Christine Frankscbc51fa2017-01-04 21:00:36 -0800417 mEvaluator.setCallback(mEvaluatorCallback);
Hans Boehm31ea2522016-11-23 17:47:02 -0800418 mResultText.setEvaluator(mEvaluator, Evaluator.MAIN_INDEX);
419 KeyMaps.setActivity(this);
Justin Klaassend48b7562015-04-16 16:51:38 -0700420
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100421 mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700422 mDeleteButton = findViewById(R.id.del);
423 mClearButton = findViewById(R.id.clr);
Christine Franksbd90b792016-11-22 10:28:26 -0800424 final View numberPad = findViewById(R.id.pad_numeric);
425 mEqualButton = numberPad.findViewById(R.id.eq);
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700426 if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) {
427 mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq);
428 }
Christine Franksbd90b792016-11-22 10:28:26 -0800429 final TextView decimalPointButton = (TextView) numberPad.findViewById(R.id.dec_point);
430 decimalPointButton.setText(getDecimalSeparator());
Justin Klaassene2711cb2015-05-28 11:13:17 -0700431
432 mInverseToggle = (TextView) findViewById(R.id.toggle_inv);
433 mModeToggle = (TextView) findViewById(R.id.toggle_mode);
434
Annie Chin26e159e2016-05-18 15:17:14 -0700435 mOneLine = mResultText.getVisibility() == View.INVISIBLE;
436
Justin Klaassen721ec842015-05-28 14:30:08 -0700437 mInvertibleButtons = new View[] {
438 findViewById(R.id.fun_sin),
439 findViewById(R.id.fun_cos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700440 findViewById(R.id.fun_tan),
441 findViewById(R.id.fun_ln),
442 findViewById(R.id.fun_log),
443 findViewById(R.id.op_sqrt)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700444 };
445 mInverseButtons = new View[] {
446 findViewById(R.id.fun_arcsin),
447 findViewById(R.id.fun_arccos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700448 findViewById(R.id.fun_arctan),
449 findViewById(R.id.fun_exp),
450 findViewById(R.id.fun_10pow),
451 findViewById(R.id.op_sqr)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700452 };
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700453
Annie Chin09547532016-10-14 10:59:07 -0700454 mDragLayout = (DragLayout) findViewById(R.id.drag_layout);
Annie Chind0f87d22016-10-24 09:04:12 -0700455 mDragLayout.removeDragCallback(mDragCallback);
456 mDragLayout.addDragCallback(mDragCallback);
Annie Chin9a211132016-11-30 12:52:06 -0800457 mDragLayout.setCloseCallback(mCloseCallback);
Annie Chin09547532016-10-14 10:59:07 -0700458
Annie Chind0f87d22016-10-24 09:04:12 -0700459 mHistoryFrame = (FrameLayout) findViewById(R.id.history_frame);
Annie Chin09547532016-10-14 10:59:07 -0700460
Christine Franks1d99be12016-11-14 14:00:36 -0800461 mFormulaText.setOnContextMenuClickListener(mOnFormulaContextMenuClickListener);
462 mFormulaText.setOnDisplayMemoryOperationsListener(mOnDisplayMemoryOperationsListener);
463
Hans Boehm08e8f322015-04-21 13:18:38 -0700464 mFormulaText.setOnTextSizeChangeListener(this);
Annie Chine918fd22016-03-09 11:07:54 -0800465 mFormulaText.addTextChangedListener(mFormulaTextWatcher);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700466 mDeleteButton.setOnLongClickListener(this);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700467
Hans Boehm31ea2522016-11-23 17:47:02 -0800468 if (savedInstanceState != null) {
469 restoreInstanceState(savedInstanceState);
Christine Frankseeff27f2016-07-29 12:05:29 -0700470 } else {
Hans Boehm31ea2522016-11-23 17:47:02 -0800471 mCurrentState = CalculatorState.INPUT;
472 mEvaluator.clearMain();
Christine Frankseeff27f2016-07-29 12:05:29 -0700473 showAndMaybeHideToolbar();
Hans Boehm31ea2522016-11-23 17:47:02 -0800474 onInverseToggled(false);
Christine Frankseeff27f2016-07-29 12:05:29 -0700475 }
Hans Boehm31ea2522016-11-23 17:47:02 -0800476 restoreDisplay();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700477 }
478
479 @Override
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800480 protected void onResume() {
481 super.onResume();
Christine Frankseeff27f2016-07-29 12:05:29 -0700482 if (mDisplayView.isToolbarVisible()) {
483 showAndMaybeHideToolbar();
484 }
Annie Chineb36f952016-12-08 17:27:19 -0800485 // If HistoryFragment is showing, hide the main Calculator elements from accessibility.
486 // This is because Talkback does not use visibility as a cue for RelativeLayout elements,
487 // and RelativeLayout is the base class of DragLayout.
488 // If we did not do this, it would be possible to traverse to main Calculator elements from
489 // HistoryFragment.
490 mMainCalculator.setImportantForAccessibility(
491 mDragLayout.isOpen() ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
492 : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800493 }
494
495 @Override
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700496 protected void onSaveInstanceState(@NonNull Bundle outState) {
Hans Boehm40125442016-01-22 10:35:35 -0800497 mEvaluator.cancelAll(true);
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700498 // If there's an animation in progress, cancel it first to ensure our state is up-to-date.
499 if (mCurrentAnimator != null) {
500 mCurrentAnimator.cancel();
501 }
502
Justin Klaassen4b3af052014-05-27 17:53:10 -0700503 super.onSaveInstanceState(outState);
Hans Boehm84614952014-11-25 18:46:17 -0800504 outState.putInt(KEY_DISPLAY_STATE, mCurrentState.ordinal());
Hans Boehm760a9dc2015-04-20 10:27:12 -0700505 outState.putCharSequence(KEY_UNPROCESSED_CHARS, mUnprocessedChars);
Hans Boehm84614952014-11-25 18:46:17 -0800506 ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
507 try (ObjectOutput out = new ObjectOutputStream(byteArrayStream)) {
508 mEvaluator.saveInstanceState(out);
509 } catch (IOException e) {
510 // Impossible; No IO involved.
511 throw new AssertionError("Impossible IO exception", e);
512 }
513 outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray());
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800514 outState.putBoolean(KEY_INVERSE_MODE, mInverseToggle.isSelected());
Christine Frankseeff27f2016-07-29 12:05:29 -0700515 outState.putBoolean(KEY_SHOW_TOOLBAR, mDisplayView.isToolbarVisible());
Hans Boehme95203e2017-01-04 14:13:11 -0800516 // We must wait for asynchronous writes to complete, since outState may contain
517 // references to expressions being written.
518 mEvaluator.waitForWrites();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700519 }
520
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700521 // Set the state, updating delete label and display colors.
522 // This restores display positions on moving to INPUT.
Justin Klaassend48b7562015-04-16 16:51:38 -0700523 // But movement/animation for moving to RESULT has already been done.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700524 private void setState(CalculatorState state) {
525 if (mCurrentState != state) {
Hans Boehm84614952014-11-25 18:46:17 -0800526 if (state == CalculatorState.INPUT) {
Hans Boehmd4959e82016-11-15 18:01:28 -0800527 // We'll explicitly request evaluation from now on.
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800528 mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_NOT_EVALUATE, null);
Hans Boehm84614952014-11-25 18:46:17 -0800529 restoreDisplayPositions();
530 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700531 mCurrentState = state;
532
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700533 if (mCurrentState == CalculatorState.RESULT) {
534 // No longer do this for ERROR; allow mistakes to be corrected.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700535 mDeleteButton.setVisibility(View.GONE);
536 mClearButton.setVisibility(View.VISIBLE);
537 } else {
538 mDeleteButton.setVisibility(View.VISIBLE);
539 mClearButton.setVisibility(View.GONE);
540 }
541
Annie Chin26e159e2016-05-18 15:17:14 -0700542 if (mOneLine) {
543 if (mCurrentState == CalculatorState.RESULT
544 || mCurrentState == CalculatorState.EVALUATE
545 || mCurrentState == CalculatorState.ANIMATE) {
546 mFormulaText.setVisibility(View.VISIBLE);
547 mResultText.setVisibility(View.VISIBLE);
Annie Chin947d93b2016-06-14 10:18:54 -0700548 } else if (mCurrentState == CalculatorState.ERROR) {
549 mFormulaText.setVisibility(View.INVISIBLE);
550 mResultText.setVisibility(View.VISIBLE);
Annie Chin26e159e2016-05-18 15:17:14 -0700551 } else {
552 mFormulaText.setVisibility(View.VISIBLE);
553 mResultText.setVisibility(View.INVISIBLE);
554 }
555 }
556
Hans Boehm84614952014-11-25 18:46:17 -0800557 if (mCurrentState == CalculatorState.ERROR) {
Chenjie Yu3937b652016-06-01 23:14:26 -0700558 final int errorColor =
559 ContextCompat.getColor(this, R.color.calculator_error_color);
Hans Boehm08e8f322015-04-21 13:18:38 -0700560 mFormulaText.setTextColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700561 mResultText.setTextColor(errorColor);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700562 getWindow().setStatusBarColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700563 } else if (mCurrentState != CalculatorState.RESULT) {
Chenjie Yu3937b652016-06-01 23:14:26 -0700564 mFormulaText.setTextColor(
565 ContextCompat.getColor(this, R.color.display_formula_text_color));
566 mResultText.setTextColor(
567 ContextCompat.getColor(this, R.color.display_result_text_color));
568 getWindow().setStatusBarColor(
Annie Chin96be2462016-12-19 14:31:16 -0800569 ContextCompat.getColor(this, R.color.calculator_statusbar_color));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700570 }
Justin Klaassend48b7562015-04-16 16:51:38 -0700571
572 invalidateOptionsMenu();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700573 }
574 }
575
Annie Chin94c1bd92016-11-23 13:39:56 -0800576 public boolean isResultLayout() {
Hans Boehm31ea2522016-11-23 17:47:02 -0800577 if (mCurrentState == CalculatorState.ANIMATE) {
578 throw new AssertionError("impossible state");
579 }
580 // Note that ERROR has INPUT, not RESULT layout.
581 return mCurrentState == CalculatorState.INIT_FOR_RESULT
582 || mCurrentState == CalculatorState.RESULT;
Annie Chin70ac8ea2016-11-18 14:43:56 -0800583 }
584
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700585 @Override
Annie Chind0f87d22016-10-24 09:04:12 -0700586 protected void onDestroy() {
587 mDragLayout.removeDragCallback(mDragCallback);
588 super.onDestroy();
589 }
590
Hans Boehma5ea8eb2016-12-01 12:33:38 -0800591 /**
592 * Destroy the evaluator and close the underlying database.
593 */
594 public void destroyEvaluator() {
595 mEvaluator.destroyEvaluator();
596 }
597
Annie Chind0f87d22016-10-24 09:04:12 -0700598 @Override
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700599 public void onActionModeStarted(ActionMode mode) {
600 super.onActionModeStarted(mode);
Christine Franks7452d3a2016-10-27 13:41:18 -0700601 if (mode.getTag() == CalculatorFormula.TAG_ACTION_MODE) {
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700602 mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
603 }
604 }
605
Chenjie Yu3937b652016-06-01 23:14:26 -0700606 /**
607 * Stop any active ActionMode or ContextMenu for copy/paste actions.
608 * Return true if there was one.
609 */
610 private boolean stopActionModeOrContextMenu() {
Christine Franks7485df52016-12-01 13:18:45 -0800611 return mResultText.stopActionModeOrContextMenu()
612 || mFormulaText.stopActionModeOrContextMenu();
Hans Boehm1176f232015-05-11 16:26:03 -0700613 }
614
Justin Klaassen4b3af052014-05-27 17:53:10 -0700615 @Override
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700616 public void onUserInteraction() {
617 super.onUserInteraction();
618
619 // If there's an animation in progress, end it immediately, so the user interaction can
620 // be handled.
621 if (mCurrentAnimator != null) {
622 mCurrentAnimator.end();
623 }
624 }
625
626 @Override
Christine Franks1473ddd2016-12-01 15:02:23 -0800627 public boolean dispatchTouchEvent(MotionEvent e) {
628 if (e.getActionMasked() == MotionEvent.ACTION_DOWN) {
629 stopActionModeOrContextMenu();
630 if (mDragLayout.isOpen()) {
631 mHistoryFragment.stopActionModeOrContextMenu();
632 }
633 }
634 return super.dispatchTouchEvent(e);
635 }
636
637 @Override
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100638 public void onBackPressed() {
Chenjie Yu3937b652016-06-01 23:14:26 -0700639 if (!stopActionModeOrContextMenu()) {
Annie Chin09547532016-10-14 10:59:07 -0700640 if (mDragLayout.isOpen()) {
Christine Franks7485df52016-12-01 13:18:45 -0800641 if (!mHistoryFragment.stopActionModeOrContextMenu()) {
642 mDragLayout.setClosed();
Justin Klaassen12874e32016-12-12 07:57:47 -0800643 removeHistoryFragment();
Christine Franks7485df52016-12-01 13:18:45 -0800644 }
Annie Chin09547532016-10-14 10:59:07 -0700645 return;
646 }
Hans Boehm1176f232015-05-11 16:26:03 -0700647 if (mPadViewPager != null && mPadViewPager.getCurrentItem() != 0) {
648 // Select the previous pad.
649 mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
650 } else {
651 // If the user is currently looking at the first pad (or the pad is not paged),
652 // allow the system to handle the Back button.
653 super.onBackPressed();
654 }
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100655 }
656 }
657
658 @Override
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700659 public boolean onKeyUp(int keyCode, KeyEvent event) {
Justin Klaassen83959da2016-04-06 11:55:24 -0700660 // Allow the system to handle special key codes (e.g. "BACK" or "DPAD").
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700661 switch (keyCode) {
Justin Klaassen83959da2016-04-06 11:55:24 -0700662 case KeyEvent.KEYCODE_BACK:
Christine Franksf9ba2202016-10-20 17:20:19 -0700663 case KeyEvent.KEYCODE_ESCAPE:
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700664 case KeyEvent.KEYCODE_DPAD_UP:
665 case KeyEvent.KEYCODE_DPAD_DOWN:
666 case KeyEvent.KEYCODE_DPAD_LEFT:
667 case KeyEvent.KEYCODE_DPAD_RIGHT:
668 return super.onKeyUp(keyCode, event);
669 }
670
Chenjie Yu3937b652016-06-01 23:14:26 -0700671 // Stop the action mode or context menu if it's showing.
672 stopActionModeOrContextMenu();
Justin Klaassend12e0622016-04-27 16:26:47 -0700673
Hans Boehmced295e2016-11-17 17:30:13 -0800674 // Always cancel unrequested in-progress evaluation of the main expression, so that
675 // we don't have to worry about subsequent asynchronous completion.
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700676 // Requested in-progress evaluations are handled below.
Hans Boehm31ea2522016-11-23 17:47:02 -0800677 cancelUnrequested();
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700678
679 switch (keyCode) {
680 case KeyEvent.KEYCODE_NUMPAD_ENTER:
681 case KeyEvent.KEYCODE_ENTER:
682 case KeyEvent.KEYCODE_DPAD_CENTER:
683 mCurrentButton = mEqualButton;
684 onEquals();
685 return true;
686 case KeyEvent.KEYCODE_DEL:
687 mCurrentButton = mDeleteButton;
688 onDelete();
689 return true;
Annie Chin56bcbf12016-09-23 17:04:22 -0700690 case KeyEvent.KEYCODE_CLEAR:
691 mCurrentButton = mClearButton;
692 onClear();
693 return true;
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700694 default:
695 cancelIfEvaluating(false);
696 final int raw = event.getKeyCharacterMap().get(keyCode, event.getMetaState());
697 if ((raw & KeyCharacterMap.COMBINING_ACCENT) != 0) {
698 return true; // discard
699 }
700 // Try to discard non-printing characters and the like.
701 // The user will have to explicitly delete other junk that gets past us.
702 if (Character.isIdentifierIgnorable(raw) || Character.isWhitespace(raw)) {
703 return true;
704 }
705 char c = (char) raw;
706 if (c == '=') {
707 mCurrentButton = mEqualButton;
708 onEquals();
709 } else {
710 addChars(String.valueOf(c), true);
711 redisplayAfterFormulaChange();
712 }
713 return true;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700714 }
715 }
716
Justin Klaassene2711cb2015-05-28 11:13:17 -0700717 /**
718 * Invoked whenever the inverse button is toggled to update the UI.
719 *
720 * @param showInverse {@code true} if inverse functions should be shown
721 */
722 private void onInverseToggled(boolean showInverse) {
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800723 mInverseToggle.setSelected(showInverse);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700724 if (showInverse) {
725 mInverseToggle.setContentDescription(getString(R.string.desc_inv_on));
Justin Klaassen721ec842015-05-28 14:30:08 -0700726 for (View invertibleButton : mInvertibleButtons) {
727 invertibleButton.setVisibility(View.GONE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700728 }
729 for (View inverseButton : mInverseButtons) {
730 inverseButton.setVisibility(View.VISIBLE);
731 }
732 } else {
733 mInverseToggle.setContentDescription(getString(R.string.desc_inv_off));
Justin Klaassen721ec842015-05-28 14:30:08 -0700734 for (View invertibleButton : mInvertibleButtons) {
735 invertibleButton.setVisibility(View.VISIBLE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700736 }
737 for (View inverseButton : mInverseButtons) {
738 inverseButton.setVisibility(View.GONE);
739 }
740 }
741 }
742
743 /**
Christine Frankseeff27f2016-07-29 12:05:29 -0700744 * Invoked whenever the deg/rad mode may have changed to update the UI. Note that the mode has
745 * not necessarily actually changed where this is invoked.
Justin Klaassene2711cb2015-05-28 11:13:17 -0700746 *
747 * @param degreeMode {@code true} if in degree mode
748 */
749 private void onModeChanged(boolean degreeMode) {
750 if (degreeMode) {
Justin Klaassend48b7562015-04-16 16:51:38 -0700751 mModeView.setText(R.string.mode_deg);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700752 mModeView.setContentDescription(getString(R.string.desc_mode_deg));
753
754 mModeToggle.setText(R.string.mode_rad);
755 mModeToggle.setContentDescription(getString(R.string.desc_switch_rad));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700756 } else {
Justin Klaassend48b7562015-04-16 16:51:38 -0700757 mModeView.setText(R.string.mode_rad);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700758 mModeView.setContentDescription(getString(R.string.desc_mode_rad));
759
760 mModeToggle.setText(R.string.mode_deg);
761 mModeToggle.setContentDescription(getString(R.string.desc_switch_deg));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700762 }
763 }
Hans Boehm84614952014-11-25 18:46:17 -0800764
Justin Klaassen12874e32016-12-12 07:57:47 -0800765 private void removeHistoryFragment() {
Annie Chin06fd3cf2016-11-07 16:04:33 -0800766 final FragmentManager manager = getFragmentManager();
Annie Chineb36f952016-12-08 17:27:19 -0800767 if (manager != null && !manager.isDestroyed()) {
768 manager.popBackStackImmediate(HistoryFragment.TAG,
769 FragmentManager.POP_BACK_STACK_INCLUSIVE);
Annie Chin06fd3cf2016-11-07 16:04:33 -0800770 }
Annie Chineb36f952016-12-08 17:27:19 -0800771
772 // When HistoryFragment is hidden, the main Calculator is important for accessibility again.
773 mMainCalculator.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
Annie Chin06fd3cf2016-11-07 16:04:33 -0800774 }
Annie Chin9a211132016-11-30 12:52:06 -0800775
Hans Boehm5d79d102015-09-16 16:33:47 -0700776 /**
777 * Switch to INPUT from RESULT state in response to input of the specified button_id.
778 * View.NO_ID is treated as an incomplete function id.
779 */
780 private void switchToInput(int button_id) {
781 if (KeyMaps.isBinary(button_id) || KeyMaps.isSuffix(button_id)) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700782 mEvaluator.collapse(mEvaluator.getMaxIndex() /* Most recent history entry */);
Hans Boehm5d79d102015-09-16 16:33:47 -0700783 } else {
784 announceClearedForAccessibility();
Hans Boehm8f051c32016-10-03 16:53:58 -0700785 mEvaluator.clearMain();
Hans Boehm5d79d102015-09-16 16:33:47 -0700786 }
787 setState(CalculatorState.INPUT);
788 }
789
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700790 // Add the given button id to input expression.
791 // If appropriate, clear the expression before doing so.
792 private void addKeyToExpr(int id) {
793 if (mCurrentState == CalculatorState.ERROR) {
794 setState(CalculatorState.INPUT);
795 } else if (mCurrentState == CalculatorState.RESULT) {
Hans Boehm5d79d102015-09-16 16:33:47 -0700796 switchToInput(id);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700797 }
798 if (!mEvaluator.append(id)) {
799 // TODO: Some user visible feedback?
800 }
801 }
802
Hans Boehm017de982015-06-10 17:46:03 -0700803 /**
804 * Add the given button id to input expression, assuming it was explicitly
805 * typed/touched.
806 * We perform slightly more aggressive correction than in pasted expressions.
807 */
808 private void addExplicitKeyToExpr(int id) {
809 if (mCurrentState == CalculatorState.INPUT && id == R.id.op_sub) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700810 mEvaluator.getExpr(Evaluator.MAIN_INDEX).removeTrailingAdditiveOperators();
Hans Boehm017de982015-06-10 17:46:03 -0700811 }
812 addKeyToExpr(id);
813 }
814
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800815 public void evaluateInstantIfNecessary() {
816 if (mCurrentState == CalculatorState.INPUT
817 && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
818 mEvaluator.evaluateAndNotify(Evaluator.MAIN_INDEX, this, mResultText);
819 }
820 }
821
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700822 private void redisplayAfterFormulaChange() {
823 // TODO: Could do this more incrementally.
824 redisplayFormula();
825 setState(CalculatorState.INPUT);
Hans Boehm8f051c32016-10-03 16:53:58 -0700826 mResultText.clear();
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800827 if (haveUnprocessed()) {
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800828 // Force reevaluation when text is deleted, even if expression is unchanged.
829 mEvaluator.touch();
830 } else {
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800831 evaluateInstantIfNecessary();
Hans Boehmc023b732015-04-29 11:30:47 -0700832 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700833 }
834
Hans Boehm52d477a2016-04-01 17:42:50 -0700835 /**
836 * Show the toolbar.
837 * Automatically hide it again if it's not relevant to current formula.
838 */
839 private void showAndMaybeHideToolbar() {
840 final boolean shouldBeVisible =
841 mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
842 mDisplayView.showToolbar(!shouldBeVisible);
843 }
844
845 /**
846 * Display or hide the toolbar depending on calculator state.
847 */
848 private void showOrHideToolbar() {
849 final boolean shouldBeVisible =
850 mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
851 if (shouldBeVisible) {
852 mDisplayView.showToolbar(false);
853 } else {
854 mDisplayView.hideToolbar();
855 }
856 }
857
Justin Klaassen4b3af052014-05-27 17:53:10 -0700858 public void onButtonClick(View view) {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700859 // Any animation is ended before we get here.
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700860 mCurrentButton = view;
Chenjie Yu3937b652016-06-01 23:14:26 -0700861 stopActionModeOrContextMenu();
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800862
Hans Boehmc1ea0912015-06-19 15:05:07 -0700863 // See onKey above for the rationale behind some of the behavior below:
Hans Boehm31ea2522016-11-23 17:47:02 -0800864 cancelUnrequested();
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800865
Justin Klaassend48b7562015-04-16 16:51:38 -0700866 final int id = view.getId();
Hans Boehm84614952014-11-25 18:46:17 -0800867 switch (id) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700868 case R.id.eq:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700869 onEquals();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700870 break;
871 case R.id.del:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700872 onDelete();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700873 break;
874 case R.id.clr:
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700875 onClear();
Hans Boehm52d477a2016-04-01 17:42:50 -0700876 return; // Toolbar visibility adjusted at end of animation.
Justin Klaassene2711cb2015-05-28 11:13:17 -0700877 case R.id.toggle_inv:
878 final boolean selected = !mInverseToggle.isSelected();
879 mInverseToggle.setSelected(selected);
880 onInverseToggled(selected);
Hans Boehmc1ea0912015-06-19 15:05:07 -0700881 if (mCurrentState == CalculatorState.RESULT) {
882 mResultText.redisplay(); // In case we cancelled reevaluation.
883 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700884 break;
885 case R.id.toggle_mode:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700886 cancelIfEvaluating(false);
Hans Boehm8f051c32016-10-03 16:53:58 -0700887 final boolean mode = !mEvaluator.getDegreeMode(Evaluator.MAIN_INDEX);
888 if (mCurrentState == CalculatorState.RESULT
889 && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasTrigFuncs()) {
890 // Capture current result evaluated in old mode.
891 mEvaluator.collapse(mEvaluator.getMaxIndex());
Hans Boehmbfe8c222015-04-02 16:26:07 -0700892 redisplayFormula();
893 }
894 // In input mode, we reinterpret already entered trig functions.
895 mEvaluator.setDegreeMode(mode);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700896 onModeChanged(mode);
Christine Frankseeff27f2016-07-29 12:05:29 -0700897 // Show the toolbar to highlight the mode change.
898 showAndMaybeHideToolbar();
Hans Boehmbfe8c222015-04-02 16:26:07 -0700899 setState(CalculatorState.INPUT);
Justin Klaassen44595162015-05-28 17:55:20 -0700900 mResultText.clear();
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800901 if (!haveUnprocessed()) {
902 evaluateInstantIfNecessary();
Hans Boehmc023b732015-04-29 11:30:47 -0700903 }
Christine Frankseeff27f2016-07-29 12:05:29 -0700904 return;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700905 default:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700906 cancelIfEvaluating(false);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800907 if (haveUnprocessed()) {
908 // For consistency, append as uninterpreted characters.
909 // This may actually be useful for a left parenthesis.
910 addChars(KeyMaps.toString(this, id), true);
911 } else {
912 addExplicitKeyToExpr(id);
913 redisplayAfterFormulaChange();
914 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700915 break;
916 }
Hans Boehm52d477a2016-04-01 17:42:50 -0700917 showOrHideToolbar();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700918 }
919
Hans Boehm84614952014-11-25 18:46:17 -0800920 void redisplayFormula() {
Hans Boehm8f051c32016-10-03 16:53:58 -0700921 SpannableStringBuilder formula
922 = mEvaluator.getExpr(Evaluator.MAIN_INDEX).toSpannableStringBuilder(this);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700923 if (mUnprocessedChars != null) {
924 // Add and highlight characters we couldn't process.
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700925 formula.append(mUnprocessedChars, mUnprocessedColorSpan,
926 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700927 }
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700928 mFormulaText.changeTextTo(formula);
Annie Chinf360ef02016-03-10 13:45:39 -0800929 mFormulaText.setContentDescription(TextUtils.isEmpty(formula)
Justin Klaassend1831412016-07-19 21:59:10 -0700930 ? getString(R.string.desc_formula) : null);
Hans Boehm84614952014-11-25 18:46:17 -0800931 }
932
Justin Klaassen4b3af052014-05-27 17:53:10 -0700933 @Override
934 public boolean onLongClick(View view) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700935 mCurrentButton = view;
936
Justin Klaassen4b3af052014-05-27 17:53:10 -0700937 if (view.getId() == R.id.del) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700938 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700939 return true;
940 }
941 return false;
942 }
943
Hans Boehm84614952014-11-25 18:46:17 -0800944 // Initial evaluation completed successfully. Initiate display.
Hans Boehm8f051c32016-10-03 16:53:58 -0700945 public void onEvaluate(long index, int initDisplayPrec, int msd, int leastDigPos,
Hans Boehma0e45f32015-05-30 13:20:35 -0700946 String truncatedWholeNumber) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700947 if (index != Evaluator.MAIN_INDEX) {
948 throw new AssertionError("Unexpected evaluation result index\n");
949 }
Annie Chin37c33b62016-11-22 14:46:28 -0800950
Justin Klaassend48b7562015-04-16 16:51:38 -0700951 // Invalidate any options that may depend on the current result.
952 invalidateOptionsMenu();
953
Hans Boehm8f051c32016-10-03 16:53:58 -0700954 mResultText.onEvaluate(index, initDisplayPrec, msd, leastDigPos, truncatedWholeNumber);
Hans Boehm31ea2522016-11-23 17:47:02 -0800955 if (mCurrentState != CalculatorState.INPUT) {
956 // In EVALUATE, INIT, or INIT_FOR_RESULT state.
Hans Boehm45223152016-12-21 10:35:35 -0800957 onResult(mCurrentState == CalculatorState.EVALUATE /* animate */,
958 mCurrentState == CalculatorState.INIT_FOR_RESULT /* previously preserved */);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700959 }
Hans Boehm84614952014-11-25 18:46:17 -0800960 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700961
Hans Boehmc1ea0912015-06-19 15:05:07 -0700962 // Reset state to reflect evaluator cancellation. Invoked by evaluator.
Hans Boehm8f051c32016-10-03 16:53:58 -0700963 public void onCancelled(long index) {
964 // Index is Evaluator.MAIN_INDEX. We should be in EVALUATE state.
Hans Boehm84614952014-11-25 18:46:17 -0800965 setState(CalculatorState.INPUT);
Hans Boehm8f051c32016-10-03 16:53:58 -0700966 mResultText.onCancelled(index);
Hans Boehm84614952014-11-25 18:46:17 -0800967 }
968
969 // Reevaluation completed; ask result to redisplay current value.
Christine Frankscbc51fa2017-01-04 21:00:36 -0800970 public void onReevaluate(long index) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700971 // Index is Evaluator.MAIN_INDEX.
972 mResultText.onReevaluate(index);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700973 }
974
Justin Klaassenfed941a2014-06-09 18:42:40 +0100975 @Override
976 public void onTextSizeChanged(final TextView textView, float oldSize) {
977 if (mCurrentState != CalculatorState.INPUT) {
978 // Only animate text changes that occur from user input.
979 return;
980 }
981
982 // Calculate the values needed to perform the scale and translation animations,
983 // maintaining the same apparent baseline for the displayed text.
984 final float textScale = oldSize / textView.getTextSize();
985 final float translationX = (1.0f - textScale) *
986 (textView.getWidth() / 2.0f - textView.getPaddingEnd());
987 final float translationY = (1.0f - textScale) *
988 (textView.getHeight() / 2.0f - textView.getPaddingBottom());
989
990 final AnimatorSet animatorSet = new AnimatorSet();
991 animatorSet.playTogether(
992 ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f),
993 ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f),
994 ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f),
995 ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f));
Justin Klaassen94db7202014-06-11 11:22:31 -0700996 animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassenfed941a2014-06-09 18:42:40 +0100997 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
998 animatorSet.start();
999 }
1000
Hans Boehmc1ea0912015-06-19 15:05:07 -07001001 /**
1002 * Cancel any in-progress explicitly requested evaluations.
1003 * @param quiet suppress pop-up message. Explicit evaluation can change the expression
1004 value, and certainly changes the display, so it seems reasonable to warn.
1005 * @return true if there was such an evaluation
1006 */
1007 private boolean cancelIfEvaluating(boolean quiet) {
1008 if (mCurrentState == CalculatorState.EVALUATE) {
Hans Boehm31ea2522016-11-23 17:47:02 -08001009 mEvaluator.cancel(Evaluator.MAIN_INDEX, quiet);
Hans Boehmc1ea0912015-06-19 15:05:07 -07001010 return true;
1011 } else {
1012 return false;
1013 }
1014 }
1015
Hans Boehm31ea2522016-11-23 17:47:02 -08001016
1017 private void cancelUnrequested() {
1018 if (mCurrentState == CalculatorState.INPUT) {
1019 mEvaluator.cancel(Evaluator.MAIN_INDEX, true);
1020 }
1021 }
1022
Hans Boehmaf04c3a2016-01-27 14:50:08 -08001023 private boolean haveUnprocessed() {
1024 return mUnprocessedChars != null && !mUnprocessedChars.isEmpty();
1025 }
1026
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -07001027 private void onEquals() {
Hans Boehm56d6e762016-06-06 11:46:29 -07001028 // Ignore if in non-INPUT state, or if there are no operators.
Justin Klaassena8075af2016-07-27 15:24:45 -07001029 if (mCurrentState == CalculatorState.INPUT) {
Hans Boehmaf04c3a2016-01-27 14:50:08 -08001030 if (haveUnprocessed()) {
Justin Klaassena8075af2016-07-27 15:24:45 -07001031 setState(CalculatorState.EVALUATE);
Hans Boehm8f051c32016-10-03 16:53:58 -07001032 onError(Evaluator.MAIN_INDEX, R.string.error_syntax);
1033 } else if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
Justin Klaassena8075af2016-07-27 15:24:45 -07001034 setState(CalculatorState.EVALUATE);
Hans Boehm8f051c32016-10-03 16:53:58 -07001035 mEvaluator.requireResult(Evaluator.MAIN_INDEX, this, mResultText);
Hans Boehmaf04c3a2016-01-27 14:50:08 -08001036 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -07001037 }
1038 }
1039
1040 private void onDelete() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001041 // Delete works like backspace; remove the last character or operator from the expression.
1042 // Note that we handle keyboard delete exactly like the delete button. For
1043 // example the delete button can be used to delete a character from an incomplete
1044 // function name typed on a physical keyboard.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001045 // This should be impossible in RESULT state.
Hans Boehmc1ea0912015-06-19 15:05:07 -07001046 // If there is an in-progress explicit evaluation, just cancel it and return.
1047 if (cancelIfEvaluating(false)) return;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001048 setState(CalculatorState.INPUT);
Hans Boehmaf04c3a2016-01-27 14:50:08 -08001049 if (haveUnprocessed()) {
1050 mUnprocessedChars = mUnprocessedChars.substring(0, mUnprocessedChars.length() - 1);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001051 } else {
Hans Boehmc023b732015-04-29 11:30:47 -07001052 mEvaluator.delete();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001053 }
Hans Boehm8f051c32016-10-03 16:53:58 -07001054 if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).isEmpty() && !haveUnprocessed()) {
Hans Boehmdb6f9992015-08-19 12:32:56 -07001055 // Resulting formula won't be announced, since it's empty.
1056 announceClearedForAccessibility();
1057 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001058 redisplayAfterFormulaChange();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -07001059 }
1060
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001061 private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
Justin Klaassen06360f92014-08-28 11:08:44 -07001062 final ViewGroupOverlay groupOverlay =
1063 (ViewGroupOverlay) getWindow().getDecorView().getOverlay();
Justin Klaassen8fff1442014-06-19 10:43:29 -07001064
1065 final Rect displayRect = new Rect();
Justin Klaassen06360f92014-08-28 11:08:44 -07001066 mDisplayView.getGlobalVisibleRect(displayRect);
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001067
1068 // Make reveal cover the display and status bar.
1069 final View revealView = new View(this);
Justin Klaassen8fff1442014-06-19 10:43:29 -07001070 revealView.setBottom(displayRect.bottom);
1071 revealView.setLeft(displayRect.left);
1072 revealView.setRight(displayRect.right);
Chenjie Yu3937b652016-06-01 23:14:26 -07001073 revealView.setBackgroundColor(ContextCompat.getColor(this, colorRes));
Justin Klaassen06360f92014-08-28 11:08:44 -07001074 groupOverlay.add(revealView);
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001075
Justin Klaassen4b3af052014-05-27 17:53:10 -07001076 final int[] clearLocation = new int[2];
1077 sourceView.getLocationInWindow(clearLocation);
1078 clearLocation[0] += sourceView.getWidth() / 2;
1079 clearLocation[1] += sourceView.getHeight() / 2;
1080
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001081 final int revealCenterX = clearLocation[0] - revealView.getLeft();
1082 final int revealCenterY = clearLocation[1] - revealView.getTop();
Justin Klaassen4b3af052014-05-27 17:53:10 -07001083
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001084 final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2);
1085 final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2);
1086 final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2);
Justin Klaassen4b3af052014-05-27 17:53:10 -07001087 final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2));
1088
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001089 final Animator revealAnimator =
1090 ViewAnimationUtils.createCircularReveal(revealView,
ztenghui3d6ecaf2014-06-05 09:56:00 -07001091 revealCenterX, revealCenterY, 0.0f, revealRadius);
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001092 revealAnimator.setDuration(
Justin Klaassen4b3af052014-05-27 17:53:10 -07001093 getResources().getInteger(android.R.integer.config_longAnimTime));
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001094 revealAnimator.addListener(listener);
Justin Klaassen4b3af052014-05-27 17:53:10 -07001095
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001096 final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
Justin Klaassen4b3af052014-05-27 17:53:10 -07001097 alphaAnimator.setDuration(
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001098 getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassen4b3af052014-05-27 17:53:10 -07001099
1100 final AnimatorSet animatorSet = new AnimatorSet();
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001101 animatorSet.play(revealAnimator).before(alphaAnimator);
Justin Klaassen4b3af052014-05-27 17:53:10 -07001102 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
1103 animatorSet.addListener(new AnimatorListenerAdapter() {
1104 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -07001105 public void onAnimationEnd(Animator animator) {
Justin Klaassen8fff1442014-06-19 10:43:29 -07001106 groupOverlay.remove(revealView);
Justin Klaassen4b3af052014-05-27 17:53:10 -07001107 mCurrentAnimator = null;
1108 }
1109 });
1110
1111 mCurrentAnimator = animatorSet;
1112 animatorSet.start();
1113 }
1114
Hans Boehmdb6f9992015-08-19 12:32:56 -07001115 private void announceClearedForAccessibility() {
1116 mResultText.announceForAccessibility(getResources().getString(R.string.cleared));
Hans Boehmccc55662015-07-07 14:16:59 -07001117 }
1118
Hans Boehm9db3ee22016-11-18 10:09:47 -08001119 public void onClearAnimationEnd() {
1120 mUnprocessedChars = null;
1121 mResultText.clear();
1122 mEvaluator.clearMain();
1123 setState(CalculatorState.INPUT);
1124 redisplayFormula();
1125 }
1126
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001127 private void onClear() {
Hans Boehm8f051c32016-10-03 16:53:58 -07001128 if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).isEmpty() && !haveUnprocessed()) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001129 return;
1130 }
Hans Boehmc1ea0912015-06-19 15:05:07 -07001131 cancelIfEvaluating(true);
Hans Boehmdb6f9992015-08-19 12:32:56 -07001132 announceClearedForAccessibility();
Annie Chin96be2462016-12-19 14:31:16 -08001133 reveal(mCurrentButton, R.color.calculator_primary_color, new AnimatorListenerAdapter() {
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001134 @Override
1135 public void onAnimationEnd(Animator animation) {
Hans Boehm9db3ee22016-11-18 10:09:47 -08001136 onClearAnimationEnd();
Hans Boehm52d477a2016-04-01 17:42:50 -07001137 showOrHideToolbar();
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001138 }
1139 });
1140 }
1141
Hans Boehm84614952014-11-25 18:46:17 -08001142 // Evaluation encountered en error. Display the error.
Hans Boehm8f051c32016-10-03 16:53:58 -07001143 @Override
1144 public void onError(final long index, final int errorResourceId) {
1145 if (index != Evaluator.MAIN_INDEX) {
1146 throw new AssertionError("Unexpected error source");
1147 }
Hans Boehmfbcef702015-04-27 18:07:47 -07001148 if (mCurrentState == CalculatorState.EVALUATE) {
1149 setState(CalculatorState.ANIMATE);
Hans Boehmccc55662015-07-07 14:16:59 -07001150 mResultText.announceForAccessibility(getResources().getString(errorResourceId));
Hans Boehmfbcef702015-04-27 18:07:47 -07001151 reveal(mCurrentButton, R.color.calculator_error_color,
1152 new AnimatorListenerAdapter() {
1153 @Override
1154 public void onAnimationEnd(Animator animation) {
1155 setState(CalculatorState.ERROR);
Hans Boehm8f051c32016-10-03 16:53:58 -07001156 mResultText.onError(index, errorResourceId);
Hans Boehmfbcef702015-04-27 18:07:47 -07001157 }
1158 });
Hans Boehm31ea2522016-11-23 17:47:02 -08001159 } else if (mCurrentState == CalculatorState.INIT
1160 || mCurrentState == CalculatorState.INIT_FOR_RESULT /* very unlikely */) {
Hans Boehmfbcef702015-04-27 18:07:47 -07001161 setState(CalculatorState.ERROR);
Hans Boehm8f051c32016-10-03 16:53:58 -07001162 mResultText.onError(index, errorResourceId);
Hans Boehmc023b732015-04-29 11:30:47 -07001163 } else {
Justin Klaassen44595162015-05-28 17:55:20 -07001164 mResultText.clear();
Justin Klaassen2be4fdb2014-08-06 19:54:09 -07001165 }
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001166 }
1167
Hans Boehm84614952014-11-25 18:46:17 -08001168 // Animate movement of result into the top formula slot.
1169 // Result window now remains translated in the top slot while the result is displayed.
1170 // (We convert it back to formula use only when the user provides new input.)
Justin Klaassen44595162015-05-28 17:55:20 -07001171 // Historical note: In the Lollipop version, this invisibly and instantaneously moved
Hans Boehm84614952014-11-25 18:46:17 -08001172 // formula and result displays back at the end of the animation. We no longer do that,
1173 // so that we can continue to properly support scrolling of the result.
1174 // We assume the result already contains the text to be expanded.
Hans Boehm45223152016-12-21 10:35:35 -08001175 private void onResult(boolean animate, boolean resultWasPreserved) {
Justin Klaassen44595162015-05-28 17:55:20 -07001176 // Calculate the textSize that would be used to display the result in the formula.
1177 // For scrollable results just use the minimum textSize to maximize the number of digits
1178 // that are visible on screen.
1179 float textSize = mFormulaText.getMinimumTextSize();
1180 if (!mResultText.isScrollable()) {
1181 textSize = mFormulaText.getVariableTextSize(mResultText.getText().toString());
1182 }
1183
1184 // Scale the result to match the calculated textSize, minimizing the jump-cut transition
1185 // when a result is reused in a subsequent expression.
1186 final float resultScale = textSize / mResultText.getTextSize();
1187
1188 // Set the result's pivot to match its gravity.
1189 mResultText.setPivotX(mResultText.getWidth() - mResultText.getPaddingRight());
1190 mResultText.setPivotY(mResultText.getHeight() - mResultText.getPaddingBottom());
1191
1192 // Calculate the necessary translations so the result takes the place of the formula and
1193 // the formula moves off the top of the screen.
Annie Chin28589dc2016-06-09 17:50:51 -07001194 final float resultTranslationY = (mFormulaContainer.getBottom() - mResultText.getBottom())
1195 - (mFormulaText.getPaddingBottom() - mResultText.getPaddingBottom());
1196 float formulaTranslationY = -mFormulaContainer.getBottom();
Annie Chin26e159e2016-05-18 15:17:14 -07001197 if (mOneLine) {
1198 // Position the result text.
1199 mResultText.setY(mResultText.getBottom());
Annie Chin28589dc2016-06-09 17:50:51 -07001200 formulaTranslationY = -(findViewById(R.id.toolbar).getBottom()
1201 + mFormulaContainer.getBottom());
Annie Chin26e159e2016-05-18 15:17:14 -07001202 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001203
Justin Klaassen44595162015-05-28 17:55:20 -07001204 // Change the result's textColor to match the formula.
1205 final int formulaTextColor = mFormulaText.getCurrentTextColor();
1206
Hans Boehm45223152016-12-21 10:35:35 -08001207 if (resultWasPreserved) {
1208 // Result was previously addded to history.
1209 mEvaluator.represerve();
1210 } else {
Hans Boehma5ea8eb2016-12-01 12:33:38 -08001211 // Add current result to history.
1212 mEvaluator.preserve(true);
Hans Boehm45223152016-12-21 10:35:35 -08001213 }
Hans Boehma5ea8eb2016-12-01 12:33:38 -08001214
Hans Boehm45223152016-12-21 10:35:35 -08001215 if (animate) {
Hans Boehmccc55662015-07-07 14:16:59 -07001216 mResultText.announceForAccessibility(getResources().getString(R.string.desc_eq));
1217 mResultText.announceForAccessibility(mResultText.getText());
Hans Boehmc1ea0912015-06-19 15:05:07 -07001218 setState(CalculatorState.ANIMATE);
Hans Boehm84614952014-11-25 18:46:17 -08001219 final AnimatorSet animatorSet = new AnimatorSet();
1220 animatorSet.playTogether(
Justin Klaassen44595162015-05-28 17:55:20 -07001221 ObjectAnimator.ofPropertyValuesHolder(mResultText,
1222 PropertyValuesHolder.ofFloat(View.SCALE_X, resultScale),
1223 PropertyValuesHolder.ofFloat(View.SCALE_Y, resultScale),
1224 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, resultTranslationY)),
1225 ObjectAnimator.ofArgb(mResultText, TEXT_COLOR, formulaTextColor),
Annie Chine918fd22016-03-09 11:07:54 -08001226 ObjectAnimator.ofFloat(mFormulaContainer, View.TRANSLATION_Y,
1227 formulaTranslationY));
Justin Klaassen44595162015-05-28 17:55:20 -07001228 animatorSet.setDuration(getResources().getInteger(
1229 android.R.integer.config_longAnimTime));
Hans Boehm84614952014-11-25 18:46:17 -08001230 animatorSet.addListener(new AnimatorListenerAdapter() {
1231 @Override
Hans Boehm84614952014-11-25 18:46:17 -08001232 public void onAnimationEnd(Animator animation) {
1233 setState(CalculatorState.RESULT);
1234 mCurrentAnimator = null;
1235 }
1236 });
Justin Klaassen4b3af052014-05-27 17:53:10 -07001237
Hans Boehm84614952014-11-25 18:46:17 -08001238 mCurrentAnimator = animatorSet;
1239 animatorSet.start();
Hans Boehm8f051c32016-10-03 16:53:58 -07001240 } else /* No animation desired; get there fast when restarting */ {
Justin Klaassen44595162015-05-28 17:55:20 -07001241 mResultText.setScaleX(resultScale);
1242 mResultText.setScaleY(resultScale);
1243 mResultText.setTranslationY(resultTranslationY);
1244 mResultText.setTextColor(formulaTextColor);
Annie Chine918fd22016-03-09 11:07:54 -08001245 mFormulaContainer.setTranslationY(formulaTranslationY);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001246 setState(CalculatorState.RESULT);
Hans Boehm84614952014-11-25 18:46:17 -08001247 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001248 }
Hans Boehm84614952014-11-25 18:46:17 -08001249
1250 // Restore positions of the formula and result displays back to their original,
1251 // pre-animation state.
1252 private void restoreDisplayPositions() {
1253 // Clear result.
Justin Klaassen44595162015-05-28 17:55:20 -07001254 mResultText.setText("");
Hans Boehm84614952014-11-25 18:46:17 -08001255 // Reset all of the values modified during the animation.
Justin Klaassen44595162015-05-28 17:55:20 -07001256 mResultText.setScaleX(1.0f);
1257 mResultText.setScaleY(1.0f);
1258 mResultText.setTranslationX(0.0f);
1259 mResultText.setTranslationY(0.0f);
Annie Chine918fd22016-03-09 11:07:54 -08001260 mFormulaContainer.setTranslationY(0.0f);
Hans Boehm84614952014-11-25 18:46:17 -08001261
Hans Boehm08e8f322015-04-21 13:18:38 -07001262 mFormulaText.requestFocus();
Hans Boehm5e6a0ca2015-09-22 17:09:01 -07001263 }
1264
1265 @Override
1266 public void onClick(AlertDialogFragment fragment, int which) {
1267 if (which == DialogInterface.BUTTON_POSITIVE) {
Christine Frankscbc51fa2017-01-04 21:00:36 -08001268 if (HistoryFragment.CLEAR_DIALOG_TAG.equals(fragment.getTag())) {
Annie Chin532b77e2016-12-06 13:30:35 -08001269 // TODO: Try to preserve the current, saved, and memory expressions. How should we
1270 // handle expressions to which they refer?
Annie Chin532b77e2016-12-06 13:30:35 -08001271 mEvaluator.clearEverything();
1272 // TODO: It's not clear what we should really do here. This is an initial hack.
1273 // May want to make onClearAnimationEnd() private if/when we fix this.
1274 onClearAnimationEnd();
Christine Frankscbc51fa2017-01-04 21:00:36 -08001275 mEvaluatorCallback.onMemoryStateChanged();
Annie Chin532b77e2016-12-06 13:30:35 -08001276 onBackPressed();
Christine Frankscbc51fa2017-01-04 21:00:36 -08001277 } else if (Evaluator.TIMEOUT_DIALOG_TAG.equals(fragment.getTag())) {
Annie Chin532b77e2016-12-06 13:30:35 -08001278 // Timeout extension request.
1279 mEvaluator.setLongTimeout();
1280 } else {
1281 Log.e(TAG, "Unknown AlertDialogFragment click:" + fragment.getTag());
1282 }
Hans Boehm5e6a0ca2015-09-22 17:09:01 -07001283 }
1284 }
Hans Boehm84614952014-11-25 18:46:17 -08001285
Justin Klaassend48b7562015-04-16 16:51:38 -07001286 @Override
1287 public boolean onCreateOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -07001288 super.onCreateOptionsMenu(menu);
1289
1290 getMenuInflater().inflate(R.menu.activity_calculator, menu);
Justin Klaassend48b7562015-04-16 16:51:38 -07001291 return true;
1292 }
1293
1294 @Override
1295 public boolean onPrepareOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -07001296 super.onPrepareOptionsMenu(menu);
1297
1298 // Show the leading option when displaying a result.
1299 menu.findItem(R.id.menu_leading).setVisible(mCurrentState == CalculatorState.RESULT);
1300
1301 // Show the fraction option when displaying a rational result.
1302 menu.findItem(R.id.menu_fraction).setVisible(mCurrentState == CalculatorState.RESULT
Hans Boehm8f051c32016-10-03 16:53:58 -07001303 && mEvaluator.getResult(Evaluator.MAIN_INDEX).exactlyDisplayable());
Justin Klaassend36d63e2015-05-05 12:59:36 -07001304
Justin Klaassend48b7562015-04-16 16:51:38 -07001305 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001306 }
1307
1308 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -07001309 public boolean onOptionsItemSelected(MenuItem item) {
Hans Boehm84614952014-11-25 18:46:17 -08001310 switch (item.getItemId()) {
Annie Chinabd202f2016-10-14 14:23:45 -07001311 case R.id.menu_history:
Annie Chin09547532016-10-14 10:59:07 -07001312 showHistoryFragment(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
1313 mDragLayout.setOpen();
Annie Chinabd202f2016-10-14 14:23:45 -07001314 return true;
Justin Klaassend36d63e2015-05-05 12:59:36 -07001315 case R.id.menu_leading:
1316 displayFull();
Hans Boehm84614952014-11-25 18:46:17 -08001317 return true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001318 case R.id.menu_fraction:
1319 displayFraction();
1320 return true;
Justin Klaassend36d63e2015-05-05 12:59:36 -07001321 case R.id.menu_licenses:
1322 startActivity(new Intent(this, Licenses.class));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001323 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001324 default:
1325 return super.onOptionsItemSelected(item);
1326 }
1327 }
1328
Hans Boehm31ea2522016-11-23 17:47:02 -08001329 /**
1330 * Change evaluation state to one that's friendly to the history fragment.
1331 * Return false if that was not easily possible.
1332 */
1333 private boolean prepareForHistory() {
1334 if (mCurrentState == CalculatorState.ANIMATE) {
1335 throw new AssertionError("onUserInteraction should have ended animation");
Annie Chine5567fd2016-12-12 13:45:24 -08001336 } else if (mCurrentState == CalculatorState.EVALUATE) {
1337 // Cancel current evaluation
1338 cancelIfEvaluating(true /* quiet */ );
1339 setState(CalculatorState.INPUT);
1340 return true;
1341 } else if (mCurrentState == CalculatorState.INIT) {
Hans Boehm31ea2522016-11-23 17:47:02 -08001342 // Easiest to just refuse. Otherwise we can see a state change
1343 // while in history mode, which causes all sorts of problems.
1344 // TODO: Consider other alternatives. If we're just doing the decimal conversion
1345 // at the end of an evaluation, we could treat this as RESULT state.
1346 return false;
1347 }
1348 // We should be in INPUT, INIT_FOR_RESULT, RESULT, or ERROR state.
1349 return true;
1350 }
1351
Annie Chin09547532016-10-14 10:59:07 -07001352 private void showHistoryFragment(int transit) {
Annie Chin06fd3cf2016-11-07 16:04:33 -08001353 final FragmentManager manager = getFragmentManager();
1354 if (manager == null || manager.isDestroyed()) {
1355 return;
1356 }
Hans Boehm31ea2522016-11-23 17:47:02 -08001357 if (!prepareForHistory()) {
1358 return;
1359 }
Annie Chind0f87d22016-10-24 09:04:12 -07001360 if (!mDragLayout.isOpen()) {
Christine Franks7485df52016-12-01 13:18:45 -08001361 stopActionModeOrContextMenu();
1362
Annie Chin450de8a2016-11-23 10:03:56 -08001363 manager.beginTransaction()
Annie Chind0f87d22016-10-24 09:04:12 -07001364 .replace(R.id.history_frame, mHistoryFragment, HistoryFragment.TAG)
1365 .setTransition(transit)
1366 .addToBackStack(HistoryFragment.TAG)
1367 .commit();
Annie Chin450de8a2016-11-23 10:03:56 -08001368 manager.executePendingTransactions();
Annie Chineb36f952016-12-08 17:27:19 -08001369
1370 // When HistoryFragment is visible, hide all descendants of the main Calculator view.
1371 mMainCalculator.setImportantForAccessibility(
1372 View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
Annie Chind0f87d22016-10-24 09:04:12 -07001373 }
Annie Chin06fd3cf2016-11-07 16:04:33 -08001374 // TODO: pass current scroll position of result
Annie Chin09547532016-10-14 10:59:07 -07001375 }
1376
Christine Franks7452d3a2016-10-27 13:41:18 -07001377 private void displayMessage(String title, String message) {
Annie Chin532b77e2016-12-06 13:30:35 -08001378 AlertDialogFragment.showMessageDialog(this, title, message, null, null /* tag */);
Hans Boehm84614952014-11-25 18:46:17 -08001379 }
1380
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001381 private void displayFraction() {
Hans Boehm8f051c32016-10-03 16:53:58 -07001382 UnifiedReal result = mEvaluator.getResult(Evaluator.MAIN_INDEX);
Christine Franks7452d3a2016-10-27 13:41:18 -07001383 displayMessage(getString(R.string.menu_fraction),
1384 KeyMaps.translateResult(result.toNiceString()));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001385 }
1386
1387 // Display full result to currently evaluated precision
1388 private void displayFull() {
1389 Resources res = getResources();
Hans Boehm24c91ed2016-06-30 18:53:44 -07001390 String msg = mResultText.getFullText(true /* withSeparators */) + " ";
Justin Klaassen44595162015-05-28 17:55:20 -07001391 if (mResultText.fullTextIsExact()) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001392 msg += res.getString(R.string.exact);
1393 } else {
1394 msg += res.getString(R.string.approximate);
1395 }
Christine Franks7452d3a2016-10-27 13:41:18 -07001396 displayMessage(getString(R.string.menu_leading), msg);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001397 }
1398
Hans Boehm017de982015-06-10 17:46:03 -07001399 /**
1400 * Add input characters to the end of the expression.
1401 * Map them to the appropriate button pushes when possible. Leftover characters
1402 * are added to mUnprocessedChars, which is presumed to immediately precede the newly
1403 * added characters.
Hans Boehm65a99a42016-02-03 18:16:07 -08001404 * @param moreChars characters to be added
1405 * @param explicit these characters were explicitly typed by the user, not pasted
Hans Boehm017de982015-06-10 17:46:03 -07001406 */
1407 private void addChars(String moreChars, boolean explicit) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001408 if (mUnprocessedChars != null) {
1409 moreChars = mUnprocessedChars + moreChars;
1410 }
1411 int current = 0;
1412 int len = moreChars.length();
Hans Boehm0b9806f2015-06-29 16:07:15 -07001413 boolean lastWasDigit = false;
Hans Boehm5d79d102015-09-16 16:33:47 -07001414 if (mCurrentState == CalculatorState.RESULT && len != 0) {
1415 // Clear display immediately for incomplete function name.
1416 switchToInput(KeyMaps.keyForChar(moreChars.charAt(current)));
1417 }
Hans Boehm24c91ed2016-06-30 18:53:44 -07001418 char groupingSeparator = KeyMaps.translateResult(",").charAt(0);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001419 while (current < len) {
1420 char c = moreChars.charAt(current);
Hans Boehm24c91ed2016-06-30 18:53:44 -07001421 if (Character.isSpaceChar(c) || c == groupingSeparator) {
1422 ++current;
1423 continue;
1424 }
Hans Boehm013969e2015-04-13 20:29:47 -07001425 int k = KeyMaps.keyForChar(c);
Hans Boehm0b9806f2015-06-29 16:07:15 -07001426 if (!explicit) {
1427 int expEnd;
1428 if (lastWasDigit && current !=
1429 (expEnd = Evaluator.exponentEnd(moreChars, current))) {
1430 // Process scientific notation with 'E' when pasting, in spite of ambiguity
1431 // with base of natural log.
1432 // Otherwise the 10^x key is the user's friend.
1433 mEvaluator.addExponent(moreChars, current, expEnd);
1434 current = expEnd;
1435 lastWasDigit = false;
1436 continue;
1437 } else {
1438 boolean isDigit = KeyMaps.digVal(k) != KeyMaps.NOT_DIGIT;
1439 if (current == 0 && (isDigit || k == R.id.dec_point)
Hans Boehm8f051c32016-10-03 16:53:58 -07001440 && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasTrailingConstant()) {
Hans Boehm0b9806f2015-06-29 16:07:15 -07001441 // Refuse to concatenate pasted content to trailing constant.
1442 // This makes pasting of calculator results more consistent, whether or
1443 // not the old calculator instance is still around.
1444 addKeyToExpr(R.id.op_mul);
1445 }
1446 lastWasDigit = (isDigit || lastWasDigit && k == R.id.dec_point);
1447 }
1448 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001449 if (k != View.NO_ID) {
1450 mCurrentButton = findViewById(k);
Hans Boehm017de982015-06-10 17:46:03 -07001451 if (explicit) {
1452 addExplicitKeyToExpr(k);
1453 } else {
1454 addKeyToExpr(k);
1455 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001456 if (Character.isSurrogate(c)) {
1457 current += 2;
1458 } else {
1459 ++current;
1460 }
1461 continue;
1462 }
Hans Boehm013969e2015-04-13 20:29:47 -07001463 int f = KeyMaps.funForString(moreChars, current);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001464 if (f != View.NO_ID) {
1465 mCurrentButton = findViewById(f);
Hans Boehm017de982015-06-10 17:46:03 -07001466 if (explicit) {
1467 addExplicitKeyToExpr(f);
1468 } else {
1469 addKeyToExpr(f);
1470 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001471 if (f == R.id.op_sqrt) {
1472 // Square root entered as function; don't lose the parenthesis.
1473 addKeyToExpr(R.id.lparen);
1474 }
1475 current = moreChars.indexOf('(', current) + 1;
1476 continue;
1477 }
1478 // There are characters left, but we can't convert them to button presses.
1479 mUnprocessedChars = moreChars.substring(current);
1480 redisplayAfterFormulaChange();
Hans Boehm52d477a2016-04-01 17:42:50 -07001481 showOrHideToolbar();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001482 return;
1483 }
1484 mUnprocessedChars = null;
1485 redisplayAfterFormulaChange();
Hans Boehm52d477a2016-04-01 17:42:50 -07001486 showOrHideToolbar();
Hans Boehm84614952014-11-25 18:46:17 -08001487 }
1488
Hans Boehm8f051c32016-10-03 16:53:58 -07001489 private void clearIfNotInputState() {
1490 if (mCurrentState == CalculatorState.ERROR
1491 || mCurrentState == CalculatorState.RESULT) {
1492 setState(CalculatorState.INPUT);
1493 mEvaluator.clearMain();
1494 }
1495 }
1496
Chenjie Yu3937b652016-06-01 23:14:26 -07001497 /**
Christine Franksbd90b792016-11-22 10:28:26 -08001498 * Since we only support LTR format, using the RTL comma does not make sense.
1499 */
1500 private String getDecimalSeparator() {
1501 final char defaultSeparator = DecimalFormatSymbols.getInstance().getDecimalSeparator();
1502 final char rtlComma = '\u066b';
1503 return defaultSeparator == rtlComma ? "," : String.valueOf(defaultSeparator);
1504 }
1505
1506 /**
Chenjie Yu3937b652016-06-01 23:14:26 -07001507 * Clean up animation for context menu.
1508 */
1509 @Override
1510 public void onContextMenuClosed(Menu menu) {
1511 stopActionModeOrContextMenu();
1512 }
Christine Franks1d99be12016-11-14 14:00:36 -08001513
1514 public interface OnDisplayMemoryOperationsListener {
1515 boolean shouldDisplayMemory();
1516 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001517}