blob: 96bf44db617461b56129e3dea98051d1efa5e436 [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;
Chenjie Yu3937b652016-06-01 23:14:26 -070047import android.support.v4.content.ContextCompat;
Justin Klaassen3b4d13d2014-06-06 18:18:37 +010048import android.support.v4.view.ViewPager;
Annie Chine918fd22016-03-09 11:07:54 -080049import android.text.Editable;
Hans Boehm8a4f81c2015-07-09 10:41:25 -070050import android.text.SpannableStringBuilder;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070051import android.text.Spanned;
Annie Chinf360ef02016-03-10 13:45:39 -080052import android.text.TextUtils;
Annie Chine918fd22016-03-09 11:07:54 -080053import android.text.TextWatcher;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070054import android.text.style.ForegroundColorSpan;
Annie Chin532b77e2016-12-06 13:30:35 -080055import android.util.Log;
Justin Klaassen44595162015-05-28 17:55:20 -070056import android.util.Property;
Annie Chine918fd22016-03-09 11:07:54 -080057import android.view.ActionMode;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070058import android.view.KeyCharacterMap;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -070059import android.view.KeyEvent;
Hans Boehm84614952014-11-25 18:46:17 -080060import android.view.Menu;
61import android.view.MenuItem;
Annie Chind0f87d22016-10-24 09:04:12 -070062import android.view.MotionEvent;
Justin Klaassen4b3af052014-05-27 17:53:10 -070063import android.view.View;
64import android.view.View.OnLongClickListener;
Justin Klaassen5f2a3342014-06-11 17:40:22 -070065import android.view.ViewAnimationUtils;
Justin Klaassen8fff1442014-06-19 10:43:29 -070066import android.view.ViewGroupOverlay;
Annie Chine918fd22016-03-09 11:07:54 -080067import android.view.ViewTreeObserver;
Justin Klaassen4b3af052014-05-27 17:53:10 -070068import android.view.animation.AccelerateDecelerateInterpolator;
Annie Chind0f87d22016-10-24 09:04:12 -070069import android.widget.FrameLayout;
Annie Chine918fd22016-03-09 11:07:54 -080070import android.widget.HorizontalScrollView;
Justin Klaassenfed941a2014-06-09 18:42:40 +010071import android.widget.TextView;
Justin Klaassend48b7562015-04-16 16:51:38 -070072import android.widget.Toolbar;
Justin Klaassenfed941a2014-06-09 18:42:40 +010073
Christine Franks7452d3a2016-10-27 13:41:18 -070074import com.android.calculator2.CalculatorFormula.OnTextSizeChangeListener;
Hans Boehm84614952014-11-25 18:46:17 -080075
76import java.io.ByteArrayInputStream;
Hans Boehm84614952014-11-25 18:46:17 -080077import java.io.ByteArrayOutputStream;
Hans Boehm84614952014-11-25 18:46:17 -080078import java.io.IOException;
Justin Klaassen721ec842015-05-28 14:30:08 -070079import java.io.ObjectInput;
80import java.io.ObjectInputStream;
81import java.io.ObjectOutput;
82import java.io.ObjectOutputStream;
Christine Franksbd90b792016-11-22 10:28:26 -080083import java.text.DecimalFormatSymbols;
Justin Klaassen4b3af052014-05-27 17:53:10 -070084
Christine Franks1d99be12016-11-14 14:00:36 -080085import static com.android.calculator2.CalculatorFormula.OnFormulaContextMenuClickListener;
86
Hans Boehm8f051c32016-10-03 16:53:58 -070087public class Calculator extends Activity
Christine Franks1d99be12016-11-14 14:00:36 -080088 implements OnTextSizeChangeListener, OnLongClickListener,
Hans Boehm8f051c32016-10-03 16:53:58 -070089 AlertDialogFragment.OnClickListener, Evaluator.EvaluationListener /* for main result */ {
Justin Klaassen2be4fdb2014-08-06 19:54:09 -070090
Annie Chin9a211132016-11-30 12:52:06 -080091 private static final String TAG = "Calculator";
Justin Klaassen2be4fdb2014-08-06 19:54:09 -070092 /**
93 * Constant for an invalid resource id.
94 */
95 public static final int INVALID_RES_ID = -1;
Justin Klaassen4b3af052014-05-27 17:53:10 -070096
97 private enum CalculatorState {
Hans Boehm84614952014-11-25 18:46:17 -080098 INPUT, // Result and formula both visible, no evaluation requested,
99 // Though result may be visible on bottom line.
100 EVALUATE, // Both visible, evaluation requested, evaluation/animation incomplete.
Hans Boehmc1ea0912015-06-19 15:05:07 -0700101 // Not used for instant result evaluation.
Hans Boehm84614952014-11-25 18:46:17 -0800102 INIT, // Very temporary state used as alternative to EVALUATE
103 // during reinitialization. Do not animate on completion.
Hans Boehma5ea8eb2016-12-01 12:33:38 -0800104 INIT_FOR_RESULT, // Identical to INIT, but evaluation is known to terminate
105 // with result, and current expression has been copied to history.
Hans Boehm84614952014-11-25 18:46:17 -0800106 ANIMATE, // Result computed, animation to enlarge result window in progress.
107 RESULT, // Result displayed, formula invisible.
108 // If we are in RESULT state, the formula was evaluated without
109 // error to initial precision.
Hans Boehm8f051c32016-10-03 16:53:58 -0700110 // The current formula is now also the last history entry.
Hans Boehm84614952014-11-25 18:46:17 -0800111 ERROR // Error displayed: Formula visible, result shows error message.
112 // Display similar to INPUT state.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700113 }
Hans Boehm84614952014-11-25 18:46:17 -0800114 // Normal transition sequence is
115 // INPUT -> EVALUATE -> ANIMATE -> RESULT (or ERROR) -> INPUT
116 // A RESULT -> ERROR transition is possible in rare corner cases, in which
117 // a higher precision evaluation exposes an error. This is possible, since we
118 // initially evaluate assuming we were given a well-defined problem. If we
119 // were actually asked to compute sqrt(<extremely tiny negative number>) we produce 0
120 // unless we are asked for enough precision that we can distinguish the argument from zero.
Hans Boehma5ea8eb2016-12-01 12:33:38 -0800121 // ERROR and RESULT are translated to INIT or INIT_FOR_RESULT state if the application
Hans Boehm84614952014-11-25 18:46:17 -0800122 // is restarted in that state. This leads us to recompute and redisplay the result
Hans Boehma5ea8eb2016-12-01 12:33:38 -0800123 // ASAP. We avoid saving the ANIMATE state or activating history in that state.
124 // In INIT_FOR_RESULT, and RESULT state, a copy of the current
125 // expression has been saved in the history db; in the other non-ANIMATE states,
126 // it has not.
Hans Boehm84614952014-11-25 18:46:17 -0800127 // TODO: Possibly save a bit more information, e.g. its initial display string
128 // or most significant digit position, to speed up restart.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700129
Justin Klaassen44595162015-05-28 17:55:20 -0700130 private final Property<TextView, Integer> TEXT_COLOR =
131 new Property<TextView, Integer>(Integer.class, "textColor") {
132 @Override
133 public Integer get(TextView textView) {
134 return textView.getCurrentTextColor();
135 }
136
137 @Override
138 public void set(TextView textView, Integer textColor) {
139 textView.setTextColor(textColor);
140 }
141 };
142
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800143 private static final String NAME = "Calculator";
Hans Boehm84614952014-11-25 18:46:17 -0800144 private static final String KEY_DISPLAY_STATE = NAME + "_display_state";
Hans Boehm760a9dc2015-04-20 10:27:12 -0700145 private static final String KEY_UNPROCESSED_CHARS = NAME + "_unprocessed_chars";
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800146 /**
147 * Associated value is a byte array holding the evaluator state.
148 */
Hans Boehm84614952014-11-25 18:46:17 -0800149 private static final String KEY_EVAL_STATE = NAME + "_eval_state";
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800150 private static final String KEY_INVERSE_MODE = NAME + "_inverse_mode";
Christine Frankseeff27f2016-07-29 12:05:29 -0700151 /**
152 * Associated value is an boolean holding the visibility state of the toolbar.
153 */
154 private static final String KEY_SHOW_TOOLBAR = NAME + "_show_toolbar";
Justin Klaassen741471e2014-06-11 09:43:44 -0700155
Annie Chine918fd22016-03-09 11:07:54 -0800156 private final ViewTreeObserver.OnPreDrawListener mPreDrawListener =
157 new ViewTreeObserver.OnPreDrawListener() {
158 @Override
159 public boolean onPreDraw() {
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700160 mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
Annie Chine918fd22016-03-09 11:07:54 -0800161 final ViewTreeObserver observer = mFormulaContainer.getViewTreeObserver();
162 if (observer.isAlive()) {
163 observer.removeOnPreDrawListener(this);
164 }
165 return false;
166 }
167 };
168
Christine Franks1d99be12016-11-14 14:00:36 -0800169 public final OnDisplayMemoryOperationsListener mOnDisplayMemoryOperationsListener =
170 new OnDisplayMemoryOperationsListener() {
171 @Override
172 public boolean shouldDisplayMemory() {
173 return mEvaluator.getMemoryIndex() != 0;
174 }
175 };
176
177 public final OnFormulaContextMenuClickListener mOnFormulaContextMenuClickListener =
178 new OnFormulaContextMenuClickListener() {
179 @Override
180 public boolean onPaste(ClipData clip) {
181 final ClipData.Item item = clip.getItemCount() == 0 ? null : clip.getItemAt(0);
182 if (item == null) {
183 // nothing to paste, bail early...
184 return false;
185 }
186
187 // Check if the item is a previously copied result, otherwise paste as raw text.
188 final Uri uri = item.getUri();
189 if (uri != null && mEvaluator.isLastSaved(uri)) {
190 clearIfNotInputState();
191 mEvaluator.appendExpr(mEvaluator.getSavedIndex());
192 redisplayAfterFormulaChange();
193 } else {
194 addChars(item.coerceToText(Calculator.this).toString(), false);
195 }
196 return true;
197 }
198
199 @Override
200 public void onMemoryRecall() {
201 clearIfNotInputState();
202 long memoryIndex = mEvaluator.getMemoryIndex();
203 if (memoryIndex != 0) {
204 mEvaluator.appendExpr(mEvaluator.getMemoryIndex());
205 redisplayAfterFormulaChange();
Hans Boehmcc368502016-12-09 10:44:46 -0800206 }
Christine Franks1d99be12016-11-14 14:00:36 -0800207 }
208 };
209
210
Annie Chine918fd22016-03-09 11:07:54 -0800211 private final TextWatcher mFormulaTextWatcher = new TextWatcher() {
212 @Override
213 public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
214 }
215
216 @Override
217 public void onTextChanged(CharSequence charSequence, int start, int count, int after) {
218 }
219
220 @Override
221 public void afterTextChanged(Editable editable) {
222 final ViewTreeObserver observer = mFormulaContainer.getViewTreeObserver();
223 if (observer.isAlive()) {
224 observer.removeOnPreDrawListener(mPreDrawListener);
225 observer.addOnPreDrawListener(mPreDrawListener);
226 }
227 }
228 };
229
Annie Chin9a211132016-11-30 12:52:06 -0800230 private final DragLayout.CloseCallback mCloseCallback = new DragLayout.CloseCallback() {
231 @Override
232 public void onClose() {
Justin Klaassen12874e32016-12-12 07:57:47 -0800233 removeHistoryFragment();
Annie Chin9a211132016-11-30 12:52:06 -0800234 }
235 };
236
Annie Chind0f87d22016-10-24 09:04:12 -0700237 private final DragLayout.DragCallback mDragCallback = new DragLayout.DragCallback() {
238 @Override
Annie Chin9a211132016-11-30 12:52:06 -0800239 public void onStartDraggingOpen() {
Annie Chind0f87d22016-10-24 09:04:12 -0700240 showHistoryFragment(FragmentTransaction.TRANSIT_NONE);
241 }
242
243 @Override
244 public void whileDragging(float yFraction) {
245 // no-op
246 }
247
248 @Override
Annie Chind3443222016-12-07 17:19:07 -0800249 public boolean shouldCaptureView(View view, int x, int y) {
250 return mDragLayout.isMoving()
251 || mDragLayout.isOpen()
252 || mDragLayout.isViewUnder(mDisplayView, x, y);
Annie Chind0f87d22016-10-24 09:04:12 -0700253 }
254
255 @Override
256 public int getDisplayHeight() {
257 return mDisplayView.getMeasuredHeight();
258 }
259
260 public void onLayout(int translation) {
261 mHistoryFrame.setTranslationY(translation + mDisplayView.getBottom());
262 }
263 };
264
Justin Klaassen4b3af052014-05-27 17:53:10 -0700265 private CalculatorState mCurrentState;
Hans Boehm84614952014-11-25 18:46:17 -0800266 private Evaluator mEvaluator;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700267
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800268 private CalculatorDisplay mDisplayView;
Justin Klaassend48b7562015-04-16 16:51:38 -0700269 private TextView mModeView;
Christine Franks7452d3a2016-10-27 13:41:18 -0700270 private CalculatorFormula mFormulaText;
Justin Klaassen44595162015-05-28 17:55:20 -0700271 private CalculatorResult mResultText;
Annie Chine918fd22016-03-09 11:07:54 -0800272 private HorizontalScrollView mFormulaContainer;
Annie Chin09547532016-10-14 10:59:07 -0700273 private DragLayout mDragLayout;
Annie Chind0f87d22016-10-24 09:04:12 -0700274 private FrameLayout mHistoryFrame;
Justin Klaassend48b7562015-04-16 16:51:38 -0700275
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100276 private ViewPager mPadViewPager;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700277 private View mDeleteButton;
278 private View mClearButton;
Justin Klaassend48b7562015-04-16 16:51:38 -0700279 private View mEqualButton;
Annie Chineb36f952016-12-08 17:27:19 -0800280 private View mMainCalculator;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700281
282 private TextView mInverseToggle;
283 private TextView mModeToggle;
284
Justin Klaassen721ec842015-05-28 14:30:08 -0700285 private View[] mInvertibleButtons;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700286 private View[] mInverseButtons;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700287
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700288 private View mCurrentButton;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700289 private Animator mCurrentAnimator;
290
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700291 // Characters that were recently entered at the end of the display that have not yet
292 // been added to the underlying expression.
293 private String mUnprocessedChars = null;
294
295 // Color to highlight unprocessed characters from physical keyboard.
296 // TODO: should probably match this to the error color?
297 private ForegroundColorSpan mUnprocessedColorSpan = new ForegroundColorSpan(Color.RED);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700298
Annie Chin26e159e2016-05-18 15:17:14 -0700299 // Whether the display is one line.
300 private boolean mOneLine;
301
Annie Chin09547532016-10-14 10:59:07 -0700302 private HistoryFragment mHistoryFragment = new HistoryFragment();
303
Hans Boehm31ea2522016-11-23 17:47:02 -0800304 /**
Hans Boehma5ea8eb2016-12-01 12:33:38 -0800305 * Map the old saved state to a new state reflecting requested result reevaluation.
306 */
307 private CalculatorState mapFromSaved(CalculatorState savedState) {
308 switch (savedState) {
309 case RESULT:
310 case INIT_FOR_RESULT:
311 // Evaluation is expected to terminate normally.
312 return CalculatorState.INIT_FOR_RESULT;
313 case ERROR:
314 case INIT:
315 return CalculatorState.INIT;
316 case EVALUATE:
317 case INPUT:
318 return savedState;
319 default: // Includes ANIMATE state.
320 throw new AssertionError("Impossible saved state");
321 }
322 }
323
324 /**
Hans Boehm31ea2522016-11-23 17:47:02 -0800325 * Restore Evaluator state and mCurrentState from savedInstanceState.
326 * Return true if the toolbar should be visible.
327 */
328 private void restoreInstanceState(Bundle savedInstanceState) {
329 final CalculatorState savedState = CalculatorState.values()[
330 savedInstanceState.getInt(KEY_DISPLAY_STATE,
331 CalculatorState.INPUT.ordinal())];
332 setState(savedState);
333 CharSequence unprocessed = savedInstanceState.getCharSequence(KEY_UNPROCESSED_CHARS);
334 if (unprocessed != null) {
335 mUnprocessedChars = unprocessed.toString();
336 }
337 byte[] state = savedInstanceState.getByteArray(KEY_EVAL_STATE);
338 if (state != null) {
339 try (ObjectInput in = new ObjectInputStream(new ByteArrayInputStream(state))) {
340 mEvaluator.restoreInstanceState(in);
341 } catch (Throwable ignored) {
342 // When in doubt, revert to clean state
343 mCurrentState = CalculatorState.INPUT;
344 mEvaluator.clearMain();
345 }
346 }
347 if (savedInstanceState.getBoolean(KEY_SHOW_TOOLBAR, true)) {
348 showAndMaybeHideToolbar();
349 } else {
350 mDisplayView.hideToolbar();
351 }
352 onInverseToggled(savedInstanceState.getBoolean(KEY_INVERSE_MODE));
353 // TODO: We're currently not saving and restoring scroll position.
354 // We probably should. Details may require care to deal with:
355 // - new display size
356 // - slow recomputation if we've scrolled far.
357 }
358
359 private void restoreDisplay() {
360 onModeChanged(mEvaluator.getDegreeMode(Evaluator.MAIN_INDEX));
361 if (mCurrentState != CalculatorState.RESULT
362 && mCurrentState != CalculatorState.INIT_FOR_RESULT) {
363 redisplayFormula();
364 }
365 if (mCurrentState == CalculatorState.INPUT) {
366 // This resultText will explicitly call evaluateAndNotify when ready.
367 mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_EVALUATE, this);
368 } else {
369 // Just reevaluate.
Hans Boehma5ea8eb2016-12-01 12:33:38 -0800370 setState(mapFromSaved(mCurrentState));
Hans Boehm31ea2522016-11-23 17:47:02 -0800371 // Request evaluation when we know display width.
372 mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_REQUIRE, this);
373 }
374 }
375
Justin Klaassen4b3af052014-05-27 17:53:10 -0700376 @Override
377 protected void onCreate(Bundle savedInstanceState) {
378 super.onCreate(savedInstanceState);
Hans Boehm31ea2522016-11-23 17:47:02 -0800379
Annie Chin09547532016-10-14 10:59:07 -0700380 setContentView(R.layout.activity_calculator_main);
Justin Klaassend48b7562015-04-16 16:51:38 -0700381 setActionBar((Toolbar) findViewById(R.id.toolbar));
382
383 // Hide all default options in the ActionBar.
384 getActionBar().setDisplayOptions(0);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700385
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800386 // Ensure the toolbar stays visible while the options menu is displayed.
387 getActionBar().addOnMenuVisibilityListener(new ActionBar.OnMenuVisibilityListener() {
388 @Override
389 public void onMenuVisibilityChanged(boolean isVisible) {
390 mDisplayView.setForceToolbarVisible(isVisible);
391 }
392 });
393
Annie Chineb36f952016-12-08 17:27:19 -0800394 mMainCalculator = findViewById(R.id.main_calculator);
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800395 mDisplayView = (CalculatorDisplay) findViewById(R.id.display);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700396 mModeView = (TextView) findViewById(R.id.mode);
Christine Franks7452d3a2016-10-27 13:41:18 -0700397 mFormulaText = (CalculatorFormula) findViewById(R.id.formula);
Justin Klaassen44595162015-05-28 17:55:20 -0700398 mResultText = (CalculatorResult) findViewById(R.id.result);
Annie Chine918fd22016-03-09 11:07:54 -0800399 mFormulaContainer = (HorizontalScrollView) findViewById(R.id.formula_container);
Hans Boehm31ea2522016-11-23 17:47:02 -0800400 mEvaluator = Evaluator.getInstance(this);
401 mResultText.setEvaluator(mEvaluator, Evaluator.MAIN_INDEX);
402 KeyMaps.setActivity(this);
Justin Klaassend48b7562015-04-16 16:51:38 -0700403
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100404 mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700405 mDeleteButton = findViewById(R.id.del);
406 mClearButton = findViewById(R.id.clr);
Christine Franksbd90b792016-11-22 10:28:26 -0800407 final View numberPad = findViewById(R.id.pad_numeric);
408 mEqualButton = numberPad.findViewById(R.id.eq);
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700409 if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) {
410 mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq);
411 }
Christine Franksbd90b792016-11-22 10:28:26 -0800412 final TextView decimalPointButton = (TextView) numberPad.findViewById(R.id.dec_point);
413 decimalPointButton.setText(getDecimalSeparator());
Justin Klaassene2711cb2015-05-28 11:13:17 -0700414
415 mInverseToggle = (TextView) findViewById(R.id.toggle_inv);
416 mModeToggle = (TextView) findViewById(R.id.toggle_mode);
417
Annie Chin26e159e2016-05-18 15:17:14 -0700418 mOneLine = mResultText.getVisibility() == View.INVISIBLE;
419
Justin Klaassen721ec842015-05-28 14:30:08 -0700420 mInvertibleButtons = new View[] {
421 findViewById(R.id.fun_sin),
422 findViewById(R.id.fun_cos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700423 findViewById(R.id.fun_tan),
424 findViewById(R.id.fun_ln),
425 findViewById(R.id.fun_log),
426 findViewById(R.id.op_sqrt)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700427 };
428 mInverseButtons = new View[] {
429 findViewById(R.id.fun_arcsin),
430 findViewById(R.id.fun_arccos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700431 findViewById(R.id.fun_arctan),
432 findViewById(R.id.fun_exp),
433 findViewById(R.id.fun_10pow),
434 findViewById(R.id.op_sqr)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700435 };
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700436
Annie Chin09547532016-10-14 10:59:07 -0700437 mDragLayout = (DragLayout) findViewById(R.id.drag_layout);
Annie Chind0f87d22016-10-24 09:04:12 -0700438 mDragLayout.removeDragCallback(mDragCallback);
439 mDragLayout.addDragCallback(mDragCallback);
Annie Chin9a211132016-11-30 12:52:06 -0800440 mDragLayout.setCloseCallback(mCloseCallback);
Annie Chin09547532016-10-14 10:59:07 -0700441
Annie Chind0f87d22016-10-24 09:04:12 -0700442 mHistoryFrame = (FrameLayout) findViewById(R.id.history_frame);
Annie Chin09547532016-10-14 10:59:07 -0700443
Christine Franks1d99be12016-11-14 14:00:36 -0800444 mFormulaText.setOnContextMenuClickListener(mOnFormulaContextMenuClickListener);
445 mFormulaText.setOnDisplayMemoryOperationsListener(mOnDisplayMemoryOperationsListener);
446
Hans Boehm08e8f322015-04-21 13:18:38 -0700447 mFormulaText.setOnTextSizeChangeListener(this);
Annie Chine918fd22016-03-09 11:07:54 -0800448 mFormulaText.addTextChangedListener(mFormulaTextWatcher);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700449 mDeleteButton.setOnLongClickListener(this);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700450
Hans Boehm31ea2522016-11-23 17:47:02 -0800451 if (savedInstanceState != null) {
452 restoreInstanceState(savedInstanceState);
Christine Frankseeff27f2016-07-29 12:05:29 -0700453 } else {
Hans Boehm31ea2522016-11-23 17:47:02 -0800454 mCurrentState = CalculatorState.INPUT;
455 mEvaluator.clearMain();
Christine Frankseeff27f2016-07-29 12:05:29 -0700456 showAndMaybeHideToolbar();
Hans Boehm31ea2522016-11-23 17:47:02 -0800457 onInverseToggled(false);
Christine Frankseeff27f2016-07-29 12:05:29 -0700458 }
Hans Boehm31ea2522016-11-23 17:47:02 -0800459 restoreDisplay();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700460 }
461
462 @Override
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800463 protected void onResume() {
464 super.onResume();
Christine Frankseeff27f2016-07-29 12:05:29 -0700465 if (mDisplayView.isToolbarVisible()) {
466 showAndMaybeHideToolbar();
467 }
Annie Chineb36f952016-12-08 17:27:19 -0800468 // If HistoryFragment is showing, hide the main Calculator elements from accessibility.
469 // This is because Talkback does not use visibility as a cue for RelativeLayout elements,
470 // and RelativeLayout is the base class of DragLayout.
471 // If we did not do this, it would be possible to traverse to main Calculator elements from
472 // HistoryFragment.
473 mMainCalculator.setImportantForAccessibility(
474 mDragLayout.isOpen() ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
475 : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800476 }
477
478 @Override
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700479 protected void onSaveInstanceState(@NonNull Bundle outState) {
Hans Boehm40125442016-01-22 10:35:35 -0800480 mEvaluator.cancelAll(true);
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700481 // If there's an animation in progress, cancel it first to ensure our state is up-to-date.
482 if (mCurrentAnimator != null) {
483 mCurrentAnimator.cancel();
484 }
485
Justin Klaassen4b3af052014-05-27 17:53:10 -0700486 super.onSaveInstanceState(outState);
Hans Boehm84614952014-11-25 18:46:17 -0800487 outState.putInt(KEY_DISPLAY_STATE, mCurrentState.ordinal());
Hans Boehm760a9dc2015-04-20 10:27:12 -0700488 outState.putCharSequence(KEY_UNPROCESSED_CHARS, mUnprocessedChars);
Hans Boehm84614952014-11-25 18:46:17 -0800489 ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
490 try (ObjectOutput out = new ObjectOutputStream(byteArrayStream)) {
491 mEvaluator.saveInstanceState(out);
492 } catch (IOException e) {
493 // Impossible; No IO involved.
494 throw new AssertionError("Impossible IO exception", e);
495 }
496 outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray());
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800497 outState.putBoolean(KEY_INVERSE_MODE, mInverseToggle.isSelected());
Christine Frankseeff27f2016-07-29 12:05:29 -0700498 outState.putBoolean(KEY_SHOW_TOOLBAR, mDisplayView.isToolbarVisible());
Hans Boehme95203e2017-01-04 14:13:11 -0800499 // We must wait for asynchronous writes to complete, since outState may contain
500 // references to expressions being written.
501 mEvaluator.waitForWrites();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700502 }
503
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700504 // Set the state, updating delete label and display colors.
505 // This restores display positions on moving to INPUT.
Justin Klaassend48b7562015-04-16 16:51:38 -0700506 // But movement/animation for moving to RESULT has already been done.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700507 private void setState(CalculatorState state) {
508 if (mCurrentState != state) {
Hans Boehm84614952014-11-25 18:46:17 -0800509 if (state == CalculatorState.INPUT) {
Hans Boehmd4959e82016-11-15 18:01:28 -0800510 // We'll explicitly request evaluation from now on.
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800511 mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_NOT_EVALUATE, null);
Hans Boehm84614952014-11-25 18:46:17 -0800512 restoreDisplayPositions();
513 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700514 mCurrentState = state;
515
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700516 if (mCurrentState == CalculatorState.RESULT) {
517 // No longer do this for ERROR; allow mistakes to be corrected.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700518 mDeleteButton.setVisibility(View.GONE);
519 mClearButton.setVisibility(View.VISIBLE);
520 } else {
521 mDeleteButton.setVisibility(View.VISIBLE);
522 mClearButton.setVisibility(View.GONE);
523 }
524
Annie Chin26e159e2016-05-18 15:17:14 -0700525 if (mOneLine) {
526 if (mCurrentState == CalculatorState.RESULT
527 || mCurrentState == CalculatorState.EVALUATE
528 || mCurrentState == CalculatorState.ANIMATE) {
529 mFormulaText.setVisibility(View.VISIBLE);
530 mResultText.setVisibility(View.VISIBLE);
Annie Chin947d93b2016-06-14 10:18:54 -0700531 } else if (mCurrentState == CalculatorState.ERROR) {
532 mFormulaText.setVisibility(View.INVISIBLE);
533 mResultText.setVisibility(View.VISIBLE);
Annie Chin26e159e2016-05-18 15:17:14 -0700534 } else {
535 mFormulaText.setVisibility(View.VISIBLE);
536 mResultText.setVisibility(View.INVISIBLE);
537 }
538 }
539
Hans Boehm84614952014-11-25 18:46:17 -0800540 if (mCurrentState == CalculatorState.ERROR) {
Chenjie Yu3937b652016-06-01 23:14:26 -0700541 final int errorColor =
542 ContextCompat.getColor(this, R.color.calculator_error_color);
Hans Boehm08e8f322015-04-21 13:18:38 -0700543 mFormulaText.setTextColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700544 mResultText.setTextColor(errorColor);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700545 getWindow().setStatusBarColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700546 } else if (mCurrentState != CalculatorState.RESULT) {
Chenjie Yu3937b652016-06-01 23:14:26 -0700547 mFormulaText.setTextColor(
548 ContextCompat.getColor(this, R.color.display_formula_text_color));
549 mResultText.setTextColor(
550 ContextCompat.getColor(this, R.color.display_result_text_color));
551 getWindow().setStatusBarColor(
Annie Chin96be2462016-12-19 14:31:16 -0800552 ContextCompat.getColor(this, R.color.calculator_statusbar_color));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700553 }
Justin Klaassend48b7562015-04-16 16:51:38 -0700554
555 invalidateOptionsMenu();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700556 }
557 }
558
Annie Chin94c1bd92016-11-23 13:39:56 -0800559 public boolean isResultLayout() {
Hans Boehm31ea2522016-11-23 17:47:02 -0800560 if (mCurrentState == CalculatorState.ANIMATE) {
561 throw new AssertionError("impossible state");
562 }
563 // Note that ERROR has INPUT, not RESULT layout.
564 return mCurrentState == CalculatorState.INIT_FOR_RESULT
565 || mCurrentState == CalculatorState.RESULT;
Annie Chin70ac8ea2016-11-18 14:43:56 -0800566 }
567
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700568 @Override
Annie Chind0f87d22016-10-24 09:04:12 -0700569 protected void onDestroy() {
570 mDragLayout.removeDragCallback(mDragCallback);
571 super.onDestroy();
572 }
573
Hans Boehma5ea8eb2016-12-01 12:33:38 -0800574 /**
575 * Destroy the evaluator and close the underlying database.
576 */
577 public void destroyEvaluator() {
578 mEvaluator.destroyEvaluator();
579 }
580
Annie Chind0f87d22016-10-24 09:04:12 -0700581 @Override
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700582 public void onActionModeStarted(ActionMode mode) {
583 super.onActionModeStarted(mode);
Christine Franks7452d3a2016-10-27 13:41:18 -0700584 if (mode.getTag() == CalculatorFormula.TAG_ACTION_MODE) {
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700585 mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
586 }
587 }
588
Chenjie Yu3937b652016-06-01 23:14:26 -0700589 /**
590 * Stop any active ActionMode or ContextMenu for copy/paste actions.
591 * Return true if there was one.
592 */
593 private boolean stopActionModeOrContextMenu() {
Christine Franks7485df52016-12-01 13:18:45 -0800594 return mResultText.stopActionModeOrContextMenu()
595 || mFormulaText.stopActionModeOrContextMenu();
Hans Boehm1176f232015-05-11 16:26:03 -0700596 }
597
Justin Klaassen4b3af052014-05-27 17:53:10 -0700598 @Override
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700599 public void onUserInteraction() {
600 super.onUserInteraction();
601
602 // If there's an animation in progress, end it immediately, so the user interaction can
603 // be handled.
604 if (mCurrentAnimator != null) {
605 mCurrentAnimator.end();
606 }
607 }
608
609 @Override
Christine Franks1473ddd2016-12-01 15:02:23 -0800610 public boolean dispatchTouchEvent(MotionEvent e) {
611 if (e.getActionMasked() == MotionEvent.ACTION_DOWN) {
612 stopActionModeOrContextMenu();
613 if (mDragLayout.isOpen()) {
614 mHistoryFragment.stopActionModeOrContextMenu();
615 }
616 }
617 return super.dispatchTouchEvent(e);
618 }
619
620 @Override
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100621 public void onBackPressed() {
Chenjie Yu3937b652016-06-01 23:14:26 -0700622 if (!stopActionModeOrContextMenu()) {
Annie Chin09547532016-10-14 10:59:07 -0700623 if (mDragLayout.isOpen()) {
Christine Franks7485df52016-12-01 13:18:45 -0800624 if (!mHistoryFragment.stopActionModeOrContextMenu()) {
625 mDragLayout.setClosed();
Justin Klaassen12874e32016-12-12 07:57:47 -0800626 removeHistoryFragment();
Christine Franks7485df52016-12-01 13:18:45 -0800627 }
Annie Chin09547532016-10-14 10:59:07 -0700628 return;
629 }
Hans Boehm1176f232015-05-11 16:26:03 -0700630 if (mPadViewPager != null && mPadViewPager.getCurrentItem() != 0) {
631 // Select the previous pad.
632 mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
633 } else {
634 // If the user is currently looking at the first pad (or the pad is not paged),
635 // allow the system to handle the Back button.
636 super.onBackPressed();
637 }
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100638 }
639 }
640
641 @Override
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700642 public boolean onKeyUp(int keyCode, KeyEvent event) {
Justin Klaassen83959da2016-04-06 11:55:24 -0700643 // Allow the system to handle special key codes (e.g. "BACK" or "DPAD").
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700644 switch (keyCode) {
Justin Klaassen83959da2016-04-06 11:55:24 -0700645 case KeyEvent.KEYCODE_BACK:
Christine Franksf9ba2202016-10-20 17:20:19 -0700646 case KeyEvent.KEYCODE_ESCAPE:
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700647 case KeyEvent.KEYCODE_DPAD_UP:
648 case KeyEvent.KEYCODE_DPAD_DOWN:
649 case KeyEvent.KEYCODE_DPAD_LEFT:
650 case KeyEvent.KEYCODE_DPAD_RIGHT:
651 return super.onKeyUp(keyCode, event);
652 }
653
Chenjie Yu3937b652016-06-01 23:14:26 -0700654 // Stop the action mode or context menu if it's showing.
655 stopActionModeOrContextMenu();
Justin Klaassend12e0622016-04-27 16:26:47 -0700656
Hans Boehmced295e2016-11-17 17:30:13 -0800657 // Always cancel unrequested in-progress evaluation of the main expression, so that
658 // we don't have to worry about subsequent asynchronous completion.
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700659 // Requested in-progress evaluations are handled below.
Hans Boehm31ea2522016-11-23 17:47:02 -0800660 cancelUnrequested();
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700661
662 switch (keyCode) {
663 case KeyEvent.KEYCODE_NUMPAD_ENTER:
664 case KeyEvent.KEYCODE_ENTER:
665 case KeyEvent.KEYCODE_DPAD_CENTER:
666 mCurrentButton = mEqualButton;
667 onEquals();
668 return true;
669 case KeyEvent.KEYCODE_DEL:
670 mCurrentButton = mDeleteButton;
671 onDelete();
672 return true;
Annie Chin56bcbf12016-09-23 17:04:22 -0700673 case KeyEvent.KEYCODE_CLEAR:
674 mCurrentButton = mClearButton;
675 onClear();
676 return true;
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700677 default:
678 cancelIfEvaluating(false);
679 final int raw = event.getKeyCharacterMap().get(keyCode, event.getMetaState());
680 if ((raw & KeyCharacterMap.COMBINING_ACCENT) != 0) {
681 return true; // discard
682 }
683 // Try to discard non-printing characters and the like.
684 // The user will have to explicitly delete other junk that gets past us.
685 if (Character.isIdentifierIgnorable(raw) || Character.isWhitespace(raw)) {
686 return true;
687 }
688 char c = (char) raw;
689 if (c == '=') {
690 mCurrentButton = mEqualButton;
691 onEquals();
692 } else {
693 addChars(String.valueOf(c), true);
694 redisplayAfterFormulaChange();
695 }
696 return true;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700697 }
698 }
699
Justin Klaassene2711cb2015-05-28 11:13:17 -0700700 /**
701 * Invoked whenever the inverse button is toggled to update the UI.
702 *
703 * @param showInverse {@code true} if inverse functions should be shown
704 */
705 private void onInverseToggled(boolean showInverse) {
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800706 mInverseToggle.setSelected(showInverse);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700707 if (showInverse) {
708 mInverseToggle.setContentDescription(getString(R.string.desc_inv_on));
Justin Klaassen721ec842015-05-28 14:30:08 -0700709 for (View invertibleButton : mInvertibleButtons) {
710 invertibleButton.setVisibility(View.GONE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700711 }
712 for (View inverseButton : mInverseButtons) {
713 inverseButton.setVisibility(View.VISIBLE);
714 }
715 } else {
716 mInverseToggle.setContentDescription(getString(R.string.desc_inv_off));
Justin Klaassen721ec842015-05-28 14:30:08 -0700717 for (View invertibleButton : mInvertibleButtons) {
718 invertibleButton.setVisibility(View.VISIBLE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700719 }
720 for (View inverseButton : mInverseButtons) {
721 inverseButton.setVisibility(View.GONE);
722 }
723 }
724 }
725
726 /**
Christine Frankseeff27f2016-07-29 12:05:29 -0700727 * Invoked whenever the deg/rad mode may have changed to update the UI. Note that the mode has
728 * not necessarily actually changed where this is invoked.
Justin Klaassene2711cb2015-05-28 11:13:17 -0700729 *
730 * @param degreeMode {@code true} if in degree mode
731 */
732 private void onModeChanged(boolean degreeMode) {
733 if (degreeMode) {
Justin Klaassend48b7562015-04-16 16:51:38 -0700734 mModeView.setText(R.string.mode_deg);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700735 mModeView.setContentDescription(getString(R.string.desc_mode_deg));
736
737 mModeToggle.setText(R.string.mode_rad);
738 mModeToggle.setContentDescription(getString(R.string.desc_switch_rad));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700739 } else {
Justin Klaassend48b7562015-04-16 16:51:38 -0700740 mModeView.setText(R.string.mode_rad);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700741 mModeView.setContentDescription(getString(R.string.desc_mode_rad));
742
743 mModeToggle.setText(R.string.mode_deg);
744 mModeToggle.setContentDescription(getString(R.string.desc_switch_deg));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700745 }
746 }
Hans Boehm84614952014-11-25 18:46:17 -0800747
Justin Klaassen12874e32016-12-12 07:57:47 -0800748 private void removeHistoryFragment() {
Annie Chin06fd3cf2016-11-07 16:04:33 -0800749 final FragmentManager manager = getFragmentManager();
Annie Chineb36f952016-12-08 17:27:19 -0800750 if (manager != null && !manager.isDestroyed()) {
751 manager.popBackStackImmediate(HistoryFragment.TAG,
752 FragmentManager.POP_BACK_STACK_INCLUSIVE);
Annie Chin06fd3cf2016-11-07 16:04:33 -0800753 }
Annie Chineb36f952016-12-08 17:27:19 -0800754
755 // When HistoryFragment is hidden, the main Calculator is important for accessibility again.
756 mMainCalculator.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
Annie Chin06fd3cf2016-11-07 16:04:33 -0800757 }
Annie Chin9a211132016-11-30 12:52:06 -0800758
Hans Boehm5d79d102015-09-16 16:33:47 -0700759 /**
760 * Switch to INPUT from RESULT state in response to input of the specified button_id.
761 * View.NO_ID is treated as an incomplete function id.
762 */
763 private void switchToInput(int button_id) {
764 if (KeyMaps.isBinary(button_id) || KeyMaps.isSuffix(button_id)) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700765 mEvaluator.collapse(mEvaluator.getMaxIndex() /* Most recent history entry */);
Hans Boehm5d79d102015-09-16 16:33:47 -0700766 } else {
767 announceClearedForAccessibility();
Hans Boehm8f051c32016-10-03 16:53:58 -0700768 mEvaluator.clearMain();
Hans Boehm5d79d102015-09-16 16:33:47 -0700769 }
770 setState(CalculatorState.INPUT);
771 }
772
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700773 // Add the given button id to input expression.
774 // If appropriate, clear the expression before doing so.
775 private void addKeyToExpr(int id) {
776 if (mCurrentState == CalculatorState.ERROR) {
777 setState(CalculatorState.INPUT);
778 } else if (mCurrentState == CalculatorState.RESULT) {
Hans Boehm5d79d102015-09-16 16:33:47 -0700779 switchToInput(id);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700780 }
781 if (!mEvaluator.append(id)) {
782 // TODO: Some user visible feedback?
783 }
784 }
785
Hans Boehm017de982015-06-10 17:46:03 -0700786 /**
787 * Add the given button id to input expression, assuming it was explicitly
788 * typed/touched.
789 * We perform slightly more aggressive correction than in pasted expressions.
790 */
791 private void addExplicitKeyToExpr(int id) {
792 if (mCurrentState == CalculatorState.INPUT && id == R.id.op_sub) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700793 mEvaluator.getExpr(Evaluator.MAIN_INDEX).removeTrailingAdditiveOperators();
Hans Boehm017de982015-06-10 17:46:03 -0700794 }
795 addKeyToExpr(id);
796 }
797
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800798 public void evaluateInstantIfNecessary() {
799 if (mCurrentState == CalculatorState.INPUT
800 && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
801 mEvaluator.evaluateAndNotify(Evaluator.MAIN_INDEX, this, mResultText);
802 }
803 }
804
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700805 private void redisplayAfterFormulaChange() {
806 // TODO: Could do this more incrementally.
807 redisplayFormula();
808 setState(CalculatorState.INPUT);
Hans Boehm8f051c32016-10-03 16:53:58 -0700809 mResultText.clear();
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800810 if (haveUnprocessed()) {
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800811 // Force reevaluation when text is deleted, even if expression is unchanged.
812 mEvaluator.touch();
813 } else {
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800814 evaluateInstantIfNecessary();
Hans Boehmc023b732015-04-29 11:30:47 -0700815 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700816 }
817
Hans Boehm52d477a2016-04-01 17:42:50 -0700818 /**
819 * Show the toolbar.
820 * Automatically hide it again if it's not relevant to current formula.
821 */
822 private void showAndMaybeHideToolbar() {
823 final boolean shouldBeVisible =
824 mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
825 mDisplayView.showToolbar(!shouldBeVisible);
826 }
827
828 /**
829 * Display or hide the toolbar depending on calculator state.
830 */
831 private void showOrHideToolbar() {
832 final boolean shouldBeVisible =
833 mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
834 if (shouldBeVisible) {
835 mDisplayView.showToolbar(false);
836 } else {
837 mDisplayView.hideToolbar();
838 }
839 }
840
Justin Klaassen4b3af052014-05-27 17:53:10 -0700841 public void onButtonClick(View view) {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700842 // Any animation is ended before we get here.
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700843 mCurrentButton = view;
Chenjie Yu3937b652016-06-01 23:14:26 -0700844 stopActionModeOrContextMenu();
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800845
Hans Boehmc1ea0912015-06-19 15:05:07 -0700846 // See onKey above for the rationale behind some of the behavior below:
Hans Boehm31ea2522016-11-23 17:47:02 -0800847 cancelUnrequested();
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800848
Justin Klaassend48b7562015-04-16 16:51:38 -0700849 final int id = view.getId();
Hans Boehm84614952014-11-25 18:46:17 -0800850 switch (id) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700851 case R.id.eq:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700852 onEquals();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700853 break;
854 case R.id.del:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700855 onDelete();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700856 break;
857 case R.id.clr:
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700858 onClear();
Hans Boehm52d477a2016-04-01 17:42:50 -0700859 return; // Toolbar visibility adjusted at end of animation.
Justin Klaassene2711cb2015-05-28 11:13:17 -0700860 case R.id.toggle_inv:
861 final boolean selected = !mInverseToggle.isSelected();
862 mInverseToggle.setSelected(selected);
863 onInverseToggled(selected);
Hans Boehmc1ea0912015-06-19 15:05:07 -0700864 if (mCurrentState == CalculatorState.RESULT) {
865 mResultText.redisplay(); // In case we cancelled reevaluation.
866 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700867 break;
868 case R.id.toggle_mode:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700869 cancelIfEvaluating(false);
Hans Boehm8f051c32016-10-03 16:53:58 -0700870 final boolean mode = !mEvaluator.getDegreeMode(Evaluator.MAIN_INDEX);
871 if (mCurrentState == CalculatorState.RESULT
872 && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasTrigFuncs()) {
873 // Capture current result evaluated in old mode.
874 mEvaluator.collapse(mEvaluator.getMaxIndex());
Hans Boehmbfe8c222015-04-02 16:26:07 -0700875 redisplayFormula();
876 }
877 // In input mode, we reinterpret already entered trig functions.
878 mEvaluator.setDegreeMode(mode);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700879 onModeChanged(mode);
Christine Frankseeff27f2016-07-29 12:05:29 -0700880 // Show the toolbar to highlight the mode change.
881 showAndMaybeHideToolbar();
Hans Boehmbfe8c222015-04-02 16:26:07 -0700882 setState(CalculatorState.INPUT);
Justin Klaassen44595162015-05-28 17:55:20 -0700883 mResultText.clear();
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800884 if (!haveUnprocessed()) {
885 evaluateInstantIfNecessary();
Hans Boehmc023b732015-04-29 11:30:47 -0700886 }
Christine Frankseeff27f2016-07-29 12:05:29 -0700887 return;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700888 default:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700889 cancelIfEvaluating(false);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800890 if (haveUnprocessed()) {
891 // For consistency, append as uninterpreted characters.
892 // This may actually be useful for a left parenthesis.
893 addChars(KeyMaps.toString(this, id), true);
894 } else {
895 addExplicitKeyToExpr(id);
896 redisplayAfterFormulaChange();
897 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700898 break;
899 }
Hans Boehm52d477a2016-04-01 17:42:50 -0700900 showOrHideToolbar();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700901 }
902
Hans Boehm84614952014-11-25 18:46:17 -0800903 void redisplayFormula() {
Hans Boehm8f051c32016-10-03 16:53:58 -0700904 SpannableStringBuilder formula
905 = mEvaluator.getExpr(Evaluator.MAIN_INDEX).toSpannableStringBuilder(this);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700906 if (mUnprocessedChars != null) {
907 // Add and highlight characters we couldn't process.
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700908 formula.append(mUnprocessedChars, mUnprocessedColorSpan,
909 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700910 }
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700911 mFormulaText.changeTextTo(formula);
Annie Chinf360ef02016-03-10 13:45:39 -0800912 mFormulaText.setContentDescription(TextUtils.isEmpty(formula)
Justin Klaassend1831412016-07-19 21:59:10 -0700913 ? getString(R.string.desc_formula) : null);
Hans Boehm84614952014-11-25 18:46:17 -0800914 }
915
Justin Klaassen4b3af052014-05-27 17:53:10 -0700916 @Override
917 public boolean onLongClick(View view) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700918 mCurrentButton = view;
919
Justin Klaassen4b3af052014-05-27 17:53:10 -0700920 if (view.getId() == R.id.del) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700921 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700922 return true;
923 }
924 return false;
925 }
926
Hans Boehm84614952014-11-25 18:46:17 -0800927 // Initial evaluation completed successfully. Initiate display.
Hans Boehm8f051c32016-10-03 16:53:58 -0700928 public void onEvaluate(long index, int initDisplayPrec, int msd, int leastDigPos,
Hans Boehma0e45f32015-05-30 13:20:35 -0700929 String truncatedWholeNumber) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700930 if (index != Evaluator.MAIN_INDEX) {
931 throw new AssertionError("Unexpected evaluation result index\n");
932 }
Annie Chin37c33b62016-11-22 14:46:28 -0800933
Justin Klaassend48b7562015-04-16 16:51:38 -0700934 // Invalidate any options that may depend on the current result.
935 invalidateOptionsMenu();
936
Hans Boehm8f051c32016-10-03 16:53:58 -0700937 mResultText.onEvaluate(index, initDisplayPrec, msd, leastDigPos, truncatedWholeNumber);
Hans Boehm31ea2522016-11-23 17:47:02 -0800938 if (mCurrentState != CalculatorState.INPUT) {
939 // In EVALUATE, INIT, or INIT_FOR_RESULT state.
Hans Boehm45223152016-12-21 10:35:35 -0800940 onResult(mCurrentState == CalculatorState.EVALUATE /* animate */,
941 mCurrentState == CalculatorState.INIT_FOR_RESULT /* previously preserved */);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700942 }
Hans Boehm84614952014-11-25 18:46:17 -0800943 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700944
Hans Boehmc1ea0912015-06-19 15:05:07 -0700945 // Reset state to reflect evaluator cancellation. Invoked by evaluator.
Hans Boehm8f051c32016-10-03 16:53:58 -0700946 public void onCancelled(long index) {
947 // Index is Evaluator.MAIN_INDEX. We should be in EVALUATE state.
Hans Boehm84614952014-11-25 18:46:17 -0800948 setState(CalculatorState.INPUT);
Hans Boehm8f051c32016-10-03 16:53:58 -0700949 mResultText.onCancelled(index);
Hans Boehm84614952014-11-25 18:46:17 -0800950 }
951
952 // Reevaluation completed; ask result to redisplay current value.
Hans Boehm8f051c32016-10-03 16:53:58 -0700953 public void onReevaluate(long index)
Hans Boehm84614952014-11-25 18:46:17 -0800954 {
Hans Boehm8f051c32016-10-03 16:53:58 -0700955 // Index is Evaluator.MAIN_INDEX.
956 mResultText.onReevaluate(index);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700957 }
958
Christine Franks61c0ed92016-12-08 15:03:53 -0800959 public void onMemoryStateChanged() {
960 mFormulaText.onMemoryStateChanged();
961 }
962
Justin Klaassenfed941a2014-06-09 18:42:40 +0100963 @Override
964 public void onTextSizeChanged(final TextView textView, float oldSize) {
965 if (mCurrentState != CalculatorState.INPUT) {
966 // Only animate text changes that occur from user input.
967 return;
968 }
969
970 // Calculate the values needed to perform the scale and translation animations,
971 // maintaining the same apparent baseline for the displayed text.
972 final float textScale = oldSize / textView.getTextSize();
973 final float translationX = (1.0f - textScale) *
974 (textView.getWidth() / 2.0f - textView.getPaddingEnd());
975 final float translationY = (1.0f - textScale) *
976 (textView.getHeight() / 2.0f - textView.getPaddingBottom());
977
978 final AnimatorSet animatorSet = new AnimatorSet();
979 animatorSet.playTogether(
980 ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f),
981 ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f),
982 ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f),
983 ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f));
Justin Klaassen94db7202014-06-11 11:22:31 -0700984 animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassenfed941a2014-06-09 18:42:40 +0100985 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
986 animatorSet.start();
987 }
988
Hans Boehmc1ea0912015-06-19 15:05:07 -0700989 /**
990 * Cancel any in-progress explicitly requested evaluations.
991 * @param quiet suppress pop-up message. Explicit evaluation can change the expression
992 value, and certainly changes the display, so it seems reasonable to warn.
993 * @return true if there was such an evaluation
994 */
995 private boolean cancelIfEvaluating(boolean quiet) {
996 if (mCurrentState == CalculatorState.EVALUATE) {
Hans Boehm31ea2522016-11-23 17:47:02 -0800997 mEvaluator.cancel(Evaluator.MAIN_INDEX, quiet);
Hans Boehmc1ea0912015-06-19 15:05:07 -0700998 return true;
999 } else {
1000 return false;
1001 }
1002 }
1003
Hans Boehm31ea2522016-11-23 17:47:02 -08001004
1005 private void cancelUnrequested() {
1006 if (mCurrentState == CalculatorState.INPUT) {
1007 mEvaluator.cancel(Evaluator.MAIN_INDEX, true);
1008 }
1009 }
1010
Hans Boehmaf04c3a2016-01-27 14:50:08 -08001011 private boolean haveUnprocessed() {
1012 return mUnprocessedChars != null && !mUnprocessedChars.isEmpty();
1013 }
1014
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -07001015 private void onEquals() {
Hans Boehm56d6e762016-06-06 11:46:29 -07001016 // Ignore if in non-INPUT state, or if there are no operators.
Justin Klaassena8075af2016-07-27 15:24:45 -07001017 if (mCurrentState == CalculatorState.INPUT) {
Hans Boehmaf04c3a2016-01-27 14:50:08 -08001018 if (haveUnprocessed()) {
Justin Klaassena8075af2016-07-27 15:24:45 -07001019 setState(CalculatorState.EVALUATE);
Hans Boehm8f051c32016-10-03 16:53:58 -07001020 onError(Evaluator.MAIN_INDEX, R.string.error_syntax);
1021 } else if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
Justin Klaassena8075af2016-07-27 15:24:45 -07001022 setState(CalculatorState.EVALUATE);
Hans Boehm8f051c32016-10-03 16:53:58 -07001023 mEvaluator.requireResult(Evaluator.MAIN_INDEX, this, mResultText);
Hans Boehmaf04c3a2016-01-27 14:50:08 -08001024 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -07001025 }
1026 }
1027
1028 private void onDelete() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001029 // Delete works like backspace; remove the last character or operator from the expression.
1030 // Note that we handle keyboard delete exactly like the delete button. For
1031 // example the delete button can be used to delete a character from an incomplete
1032 // function name typed on a physical keyboard.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001033 // This should be impossible in RESULT state.
Hans Boehmc1ea0912015-06-19 15:05:07 -07001034 // If there is an in-progress explicit evaluation, just cancel it and return.
1035 if (cancelIfEvaluating(false)) return;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001036 setState(CalculatorState.INPUT);
Hans Boehmaf04c3a2016-01-27 14:50:08 -08001037 if (haveUnprocessed()) {
1038 mUnprocessedChars = mUnprocessedChars.substring(0, mUnprocessedChars.length() - 1);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001039 } else {
Hans Boehmc023b732015-04-29 11:30:47 -07001040 mEvaluator.delete();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001041 }
Hans Boehm8f051c32016-10-03 16:53:58 -07001042 if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).isEmpty() && !haveUnprocessed()) {
Hans Boehmdb6f9992015-08-19 12:32:56 -07001043 // Resulting formula won't be announced, since it's empty.
1044 announceClearedForAccessibility();
1045 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001046 redisplayAfterFormulaChange();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -07001047 }
1048
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001049 private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
Justin Klaassen06360f92014-08-28 11:08:44 -07001050 final ViewGroupOverlay groupOverlay =
1051 (ViewGroupOverlay) getWindow().getDecorView().getOverlay();
Justin Klaassen8fff1442014-06-19 10:43:29 -07001052
1053 final Rect displayRect = new Rect();
Justin Klaassen06360f92014-08-28 11:08:44 -07001054 mDisplayView.getGlobalVisibleRect(displayRect);
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001055
1056 // Make reveal cover the display and status bar.
1057 final View revealView = new View(this);
Justin Klaassen8fff1442014-06-19 10:43:29 -07001058 revealView.setBottom(displayRect.bottom);
1059 revealView.setLeft(displayRect.left);
1060 revealView.setRight(displayRect.right);
Chenjie Yu3937b652016-06-01 23:14:26 -07001061 revealView.setBackgroundColor(ContextCompat.getColor(this, colorRes));
Justin Klaassen06360f92014-08-28 11:08:44 -07001062 groupOverlay.add(revealView);
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001063
Justin Klaassen4b3af052014-05-27 17:53:10 -07001064 final int[] clearLocation = new int[2];
1065 sourceView.getLocationInWindow(clearLocation);
1066 clearLocation[0] += sourceView.getWidth() / 2;
1067 clearLocation[1] += sourceView.getHeight() / 2;
1068
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001069 final int revealCenterX = clearLocation[0] - revealView.getLeft();
1070 final int revealCenterY = clearLocation[1] - revealView.getTop();
Justin Klaassen4b3af052014-05-27 17:53:10 -07001071
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001072 final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2);
1073 final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2);
1074 final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2);
Justin Klaassen4b3af052014-05-27 17:53:10 -07001075 final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2));
1076
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001077 final Animator revealAnimator =
1078 ViewAnimationUtils.createCircularReveal(revealView,
ztenghui3d6ecaf2014-06-05 09:56:00 -07001079 revealCenterX, revealCenterY, 0.0f, revealRadius);
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001080 revealAnimator.setDuration(
Justin Klaassen4b3af052014-05-27 17:53:10 -07001081 getResources().getInteger(android.R.integer.config_longAnimTime));
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001082 revealAnimator.addListener(listener);
Justin Klaassen4b3af052014-05-27 17:53:10 -07001083
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001084 final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
Justin Klaassen4b3af052014-05-27 17:53:10 -07001085 alphaAnimator.setDuration(
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001086 getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassen4b3af052014-05-27 17:53:10 -07001087
1088 final AnimatorSet animatorSet = new AnimatorSet();
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001089 animatorSet.play(revealAnimator).before(alphaAnimator);
Justin Klaassen4b3af052014-05-27 17:53:10 -07001090 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
1091 animatorSet.addListener(new AnimatorListenerAdapter() {
1092 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -07001093 public void onAnimationEnd(Animator animator) {
Justin Klaassen8fff1442014-06-19 10:43:29 -07001094 groupOverlay.remove(revealView);
Justin Klaassen4b3af052014-05-27 17:53:10 -07001095 mCurrentAnimator = null;
1096 }
1097 });
1098
1099 mCurrentAnimator = animatorSet;
1100 animatorSet.start();
1101 }
1102
Hans Boehmdb6f9992015-08-19 12:32:56 -07001103 private void announceClearedForAccessibility() {
1104 mResultText.announceForAccessibility(getResources().getString(R.string.cleared));
Hans Boehmccc55662015-07-07 14:16:59 -07001105 }
1106
Hans Boehm9db3ee22016-11-18 10:09:47 -08001107 public void onClearAnimationEnd() {
1108 mUnprocessedChars = null;
1109 mResultText.clear();
1110 mEvaluator.clearMain();
1111 setState(CalculatorState.INPUT);
1112 redisplayFormula();
1113 }
1114
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001115 private void onClear() {
Hans Boehm8f051c32016-10-03 16:53:58 -07001116 if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).isEmpty() && !haveUnprocessed()) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001117 return;
1118 }
Hans Boehmc1ea0912015-06-19 15:05:07 -07001119 cancelIfEvaluating(true);
Hans Boehmdb6f9992015-08-19 12:32:56 -07001120 announceClearedForAccessibility();
Annie Chin96be2462016-12-19 14:31:16 -08001121 reveal(mCurrentButton, R.color.calculator_primary_color, new AnimatorListenerAdapter() {
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001122 @Override
1123 public void onAnimationEnd(Animator animation) {
Hans Boehm9db3ee22016-11-18 10:09:47 -08001124 onClearAnimationEnd();
Hans Boehm52d477a2016-04-01 17:42:50 -07001125 showOrHideToolbar();
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001126 }
1127 });
1128 }
1129
Hans Boehm84614952014-11-25 18:46:17 -08001130 // Evaluation encountered en error. Display the error.
Hans Boehm8f051c32016-10-03 16:53:58 -07001131 @Override
1132 public void onError(final long index, final int errorResourceId) {
1133 if (index != Evaluator.MAIN_INDEX) {
1134 throw new AssertionError("Unexpected error source");
1135 }
Hans Boehmfbcef702015-04-27 18:07:47 -07001136 if (mCurrentState == CalculatorState.EVALUATE) {
1137 setState(CalculatorState.ANIMATE);
Hans Boehmccc55662015-07-07 14:16:59 -07001138 mResultText.announceForAccessibility(getResources().getString(errorResourceId));
Hans Boehmfbcef702015-04-27 18:07:47 -07001139 reveal(mCurrentButton, R.color.calculator_error_color,
1140 new AnimatorListenerAdapter() {
1141 @Override
1142 public void onAnimationEnd(Animator animation) {
1143 setState(CalculatorState.ERROR);
Hans Boehm8f051c32016-10-03 16:53:58 -07001144 mResultText.onError(index, errorResourceId);
Hans Boehmfbcef702015-04-27 18:07:47 -07001145 }
1146 });
Hans Boehm31ea2522016-11-23 17:47:02 -08001147 } else if (mCurrentState == CalculatorState.INIT
1148 || mCurrentState == CalculatorState.INIT_FOR_RESULT /* very unlikely */) {
Hans Boehmfbcef702015-04-27 18:07:47 -07001149 setState(CalculatorState.ERROR);
Hans Boehm8f051c32016-10-03 16:53:58 -07001150 mResultText.onError(index, errorResourceId);
Hans Boehmc023b732015-04-29 11:30:47 -07001151 } else {
Justin Klaassen44595162015-05-28 17:55:20 -07001152 mResultText.clear();
Justin Klaassen2be4fdb2014-08-06 19:54:09 -07001153 }
Justin Klaassen5f2a3342014-06-11 17:40:22 -07001154 }
1155
Hans Boehm84614952014-11-25 18:46:17 -08001156 // Animate movement of result into the top formula slot.
1157 // Result window now remains translated in the top slot while the result is displayed.
1158 // (We convert it back to formula use only when the user provides new input.)
Justin Klaassen44595162015-05-28 17:55:20 -07001159 // Historical note: In the Lollipop version, this invisibly and instantaneously moved
Hans Boehm84614952014-11-25 18:46:17 -08001160 // formula and result displays back at the end of the animation. We no longer do that,
1161 // so that we can continue to properly support scrolling of the result.
1162 // We assume the result already contains the text to be expanded.
Hans Boehm45223152016-12-21 10:35:35 -08001163 private void onResult(boolean animate, boolean resultWasPreserved) {
Justin Klaassen44595162015-05-28 17:55:20 -07001164 // Calculate the textSize that would be used to display the result in the formula.
1165 // For scrollable results just use the minimum textSize to maximize the number of digits
1166 // that are visible on screen.
1167 float textSize = mFormulaText.getMinimumTextSize();
1168 if (!mResultText.isScrollable()) {
1169 textSize = mFormulaText.getVariableTextSize(mResultText.getText().toString());
1170 }
1171
1172 // Scale the result to match the calculated textSize, minimizing the jump-cut transition
1173 // when a result is reused in a subsequent expression.
1174 final float resultScale = textSize / mResultText.getTextSize();
1175
1176 // Set the result's pivot to match its gravity.
1177 mResultText.setPivotX(mResultText.getWidth() - mResultText.getPaddingRight());
1178 mResultText.setPivotY(mResultText.getHeight() - mResultText.getPaddingBottom());
1179
1180 // Calculate the necessary translations so the result takes the place of the formula and
1181 // the formula moves off the top of the screen.
Annie Chin28589dc2016-06-09 17:50:51 -07001182 final float resultTranslationY = (mFormulaContainer.getBottom() - mResultText.getBottom())
1183 - (mFormulaText.getPaddingBottom() - mResultText.getPaddingBottom());
1184 float formulaTranslationY = -mFormulaContainer.getBottom();
Annie Chin26e159e2016-05-18 15:17:14 -07001185 if (mOneLine) {
1186 // Position the result text.
1187 mResultText.setY(mResultText.getBottom());
Annie Chin28589dc2016-06-09 17:50:51 -07001188 formulaTranslationY = -(findViewById(R.id.toolbar).getBottom()
1189 + mFormulaContainer.getBottom());
Annie Chin26e159e2016-05-18 15:17:14 -07001190 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001191
Justin Klaassen44595162015-05-28 17:55:20 -07001192 // Change the result's textColor to match the formula.
1193 final int formulaTextColor = mFormulaText.getCurrentTextColor();
1194
Hans Boehm45223152016-12-21 10:35:35 -08001195 if (resultWasPreserved) {
1196 // Result was previously addded to history.
1197 mEvaluator.represerve();
1198 } else {
Hans Boehma5ea8eb2016-12-01 12:33:38 -08001199 // Add current result to history.
1200 mEvaluator.preserve(true);
Hans Boehm45223152016-12-21 10:35:35 -08001201 }
Hans Boehma5ea8eb2016-12-01 12:33:38 -08001202
Hans Boehm45223152016-12-21 10:35:35 -08001203 if (animate) {
Hans Boehmccc55662015-07-07 14:16:59 -07001204 mResultText.announceForAccessibility(getResources().getString(R.string.desc_eq));
1205 mResultText.announceForAccessibility(mResultText.getText());
Hans Boehmc1ea0912015-06-19 15:05:07 -07001206 setState(CalculatorState.ANIMATE);
Hans Boehm84614952014-11-25 18:46:17 -08001207 final AnimatorSet animatorSet = new AnimatorSet();
1208 animatorSet.playTogether(
Justin Klaassen44595162015-05-28 17:55:20 -07001209 ObjectAnimator.ofPropertyValuesHolder(mResultText,
1210 PropertyValuesHolder.ofFloat(View.SCALE_X, resultScale),
1211 PropertyValuesHolder.ofFloat(View.SCALE_Y, resultScale),
1212 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, resultTranslationY)),
1213 ObjectAnimator.ofArgb(mResultText, TEXT_COLOR, formulaTextColor),
Annie Chine918fd22016-03-09 11:07:54 -08001214 ObjectAnimator.ofFloat(mFormulaContainer, View.TRANSLATION_Y,
1215 formulaTranslationY));
Justin Klaassen44595162015-05-28 17:55:20 -07001216 animatorSet.setDuration(getResources().getInteger(
1217 android.R.integer.config_longAnimTime));
Hans Boehm84614952014-11-25 18:46:17 -08001218 animatorSet.addListener(new AnimatorListenerAdapter() {
1219 @Override
Hans Boehm84614952014-11-25 18:46:17 -08001220 public void onAnimationEnd(Animator animation) {
1221 setState(CalculatorState.RESULT);
1222 mCurrentAnimator = null;
1223 }
1224 });
Justin Klaassen4b3af052014-05-27 17:53:10 -07001225
Hans Boehm84614952014-11-25 18:46:17 -08001226 mCurrentAnimator = animatorSet;
1227 animatorSet.start();
Hans Boehm8f051c32016-10-03 16:53:58 -07001228 } else /* No animation desired; get there fast when restarting */ {
Justin Klaassen44595162015-05-28 17:55:20 -07001229 mResultText.setScaleX(resultScale);
1230 mResultText.setScaleY(resultScale);
1231 mResultText.setTranslationY(resultTranslationY);
1232 mResultText.setTextColor(formulaTextColor);
Annie Chine918fd22016-03-09 11:07:54 -08001233 mFormulaContainer.setTranslationY(formulaTranslationY);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001234 setState(CalculatorState.RESULT);
Hans Boehm84614952014-11-25 18:46:17 -08001235 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001236 }
Hans Boehm84614952014-11-25 18:46:17 -08001237
1238 // Restore positions of the formula and result displays back to their original,
1239 // pre-animation state.
1240 private void restoreDisplayPositions() {
1241 // Clear result.
Justin Klaassen44595162015-05-28 17:55:20 -07001242 mResultText.setText("");
Hans Boehm84614952014-11-25 18:46:17 -08001243 // Reset all of the values modified during the animation.
Justin Klaassen44595162015-05-28 17:55:20 -07001244 mResultText.setScaleX(1.0f);
1245 mResultText.setScaleY(1.0f);
1246 mResultText.setTranslationX(0.0f);
1247 mResultText.setTranslationY(0.0f);
Annie Chine918fd22016-03-09 11:07:54 -08001248 mFormulaContainer.setTranslationY(0.0f);
Hans Boehm84614952014-11-25 18:46:17 -08001249
Hans Boehm08e8f322015-04-21 13:18:38 -07001250 mFormulaText.requestFocus();
Hans Boehm5e6a0ca2015-09-22 17:09:01 -07001251 }
1252
1253 @Override
1254 public void onClick(AlertDialogFragment fragment, int which) {
1255 if (which == DialogInterface.BUTTON_POSITIVE) {
Annie Chin532b77e2016-12-06 13:30:35 -08001256 if (fragment.getTag() == HistoryFragment.CLEAR_DIALOG_TAG) {
1257 // TODO: Try to preserve the current, saved, and memory expressions. How should we
1258 // handle expressions to which they refer?
Annie Chin532b77e2016-12-06 13:30:35 -08001259 mEvaluator.clearEverything();
1260 // TODO: It's not clear what we should really do here. This is an initial hack.
1261 // May want to make onClearAnimationEnd() private if/when we fix this.
1262 onClearAnimationEnd();
Christine Franks61c0ed92016-12-08 15:03:53 -08001263 onMemoryStateChanged();
Annie Chin532b77e2016-12-06 13:30:35 -08001264 onBackPressed();
1265 } else if (fragment.getTag() == Evaluator.TIMEOUT_DIALOG_TAG) {
1266 // Timeout extension request.
1267 mEvaluator.setLongTimeout();
1268 } else {
1269 Log.e(TAG, "Unknown AlertDialogFragment click:" + fragment.getTag());
1270 }
Hans Boehm5e6a0ca2015-09-22 17:09:01 -07001271 }
1272 }
Hans Boehm84614952014-11-25 18:46:17 -08001273
Justin Klaassend48b7562015-04-16 16:51:38 -07001274 @Override
1275 public boolean onCreateOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -07001276 super.onCreateOptionsMenu(menu);
1277
1278 getMenuInflater().inflate(R.menu.activity_calculator, menu);
Justin Klaassend48b7562015-04-16 16:51:38 -07001279 return true;
1280 }
1281
1282 @Override
1283 public boolean onPrepareOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -07001284 super.onPrepareOptionsMenu(menu);
1285
1286 // Show the leading option when displaying a result.
1287 menu.findItem(R.id.menu_leading).setVisible(mCurrentState == CalculatorState.RESULT);
1288
1289 // Show the fraction option when displaying a rational result.
1290 menu.findItem(R.id.menu_fraction).setVisible(mCurrentState == CalculatorState.RESULT
Hans Boehm8f051c32016-10-03 16:53:58 -07001291 && mEvaluator.getResult(Evaluator.MAIN_INDEX).exactlyDisplayable());
Justin Klaassend36d63e2015-05-05 12:59:36 -07001292
Justin Klaassend48b7562015-04-16 16:51:38 -07001293 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001294 }
1295
1296 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -07001297 public boolean onOptionsItemSelected(MenuItem item) {
Hans Boehm84614952014-11-25 18:46:17 -08001298 switch (item.getItemId()) {
Annie Chinabd202f2016-10-14 14:23:45 -07001299 case R.id.menu_history:
Annie Chin09547532016-10-14 10:59:07 -07001300 showHistoryFragment(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
1301 mDragLayout.setOpen();
Annie Chinabd202f2016-10-14 14:23:45 -07001302 return true;
Justin Klaassend36d63e2015-05-05 12:59:36 -07001303 case R.id.menu_leading:
1304 displayFull();
Hans Boehm84614952014-11-25 18:46:17 -08001305 return true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001306 case R.id.menu_fraction:
1307 displayFraction();
1308 return true;
Justin Klaassend36d63e2015-05-05 12:59:36 -07001309 case R.id.menu_licenses:
1310 startActivity(new Intent(this, Licenses.class));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001311 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001312 default:
1313 return super.onOptionsItemSelected(item);
1314 }
1315 }
1316
Hans Boehm31ea2522016-11-23 17:47:02 -08001317 /**
1318 * Change evaluation state to one that's friendly to the history fragment.
1319 * Return false if that was not easily possible.
1320 */
1321 private boolean prepareForHistory() {
1322 if (mCurrentState == CalculatorState.ANIMATE) {
1323 throw new AssertionError("onUserInteraction should have ended animation");
Annie Chine5567fd2016-12-12 13:45:24 -08001324 } else if (mCurrentState == CalculatorState.EVALUATE) {
1325 // Cancel current evaluation
1326 cancelIfEvaluating(true /* quiet */ );
1327 setState(CalculatorState.INPUT);
1328 return true;
1329 } else if (mCurrentState == CalculatorState.INIT) {
Hans Boehm31ea2522016-11-23 17:47:02 -08001330 // Easiest to just refuse. Otherwise we can see a state change
1331 // while in history mode, which causes all sorts of problems.
1332 // TODO: Consider other alternatives. If we're just doing the decimal conversion
1333 // at the end of an evaluation, we could treat this as RESULT state.
1334 return false;
1335 }
1336 // We should be in INPUT, INIT_FOR_RESULT, RESULT, or ERROR state.
1337 return true;
1338 }
1339
Annie Chin09547532016-10-14 10:59:07 -07001340 private void showHistoryFragment(int transit) {
Annie Chin06fd3cf2016-11-07 16:04:33 -08001341 final FragmentManager manager = getFragmentManager();
1342 if (manager == null || manager.isDestroyed()) {
1343 return;
1344 }
Hans Boehm31ea2522016-11-23 17:47:02 -08001345 if (!prepareForHistory()) {
1346 return;
1347 }
Annie Chind0f87d22016-10-24 09:04:12 -07001348 if (!mDragLayout.isOpen()) {
Christine Franks7485df52016-12-01 13:18:45 -08001349 stopActionModeOrContextMenu();
1350
Annie Chin450de8a2016-11-23 10:03:56 -08001351 manager.beginTransaction()
Annie Chind0f87d22016-10-24 09:04:12 -07001352 .replace(R.id.history_frame, mHistoryFragment, HistoryFragment.TAG)
1353 .setTransition(transit)
1354 .addToBackStack(HistoryFragment.TAG)
1355 .commit();
Annie Chin450de8a2016-11-23 10:03:56 -08001356 manager.executePendingTransactions();
Annie Chineb36f952016-12-08 17:27:19 -08001357
1358 // When HistoryFragment is visible, hide all descendants of the main Calculator view.
1359 mMainCalculator.setImportantForAccessibility(
1360 View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
Annie Chind0f87d22016-10-24 09:04:12 -07001361 }
Annie Chin06fd3cf2016-11-07 16:04:33 -08001362 // TODO: pass current scroll position of result
Annie Chin09547532016-10-14 10:59:07 -07001363 }
1364
Christine Franks7452d3a2016-10-27 13:41:18 -07001365 private void displayMessage(String title, String message) {
Annie Chin532b77e2016-12-06 13:30:35 -08001366 AlertDialogFragment.showMessageDialog(this, title, message, null, null /* tag */);
Hans Boehm84614952014-11-25 18:46:17 -08001367 }
1368
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001369 private void displayFraction() {
Hans Boehm8f051c32016-10-03 16:53:58 -07001370 UnifiedReal result = mEvaluator.getResult(Evaluator.MAIN_INDEX);
Christine Franks7452d3a2016-10-27 13:41:18 -07001371 displayMessage(getString(R.string.menu_fraction),
1372 KeyMaps.translateResult(result.toNiceString()));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001373 }
1374
1375 // Display full result to currently evaluated precision
1376 private void displayFull() {
1377 Resources res = getResources();
Hans Boehm24c91ed2016-06-30 18:53:44 -07001378 String msg = mResultText.getFullText(true /* withSeparators */) + " ";
Justin Klaassen44595162015-05-28 17:55:20 -07001379 if (mResultText.fullTextIsExact()) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001380 msg += res.getString(R.string.exact);
1381 } else {
1382 msg += res.getString(R.string.approximate);
1383 }
Christine Franks7452d3a2016-10-27 13:41:18 -07001384 displayMessage(getString(R.string.menu_leading), msg);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001385 }
1386
Hans Boehm017de982015-06-10 17:46:03 -07001387 /**
1388 * Add input characters to the end of the expression.
1389 * Map them to the appropriate button pushes when possible. Leftover characters
1390 * are added to mUnprocessedChars, which is presumed to immediately precede the newly
1391 * added characters.
Hans Boehm65a99a42016-02-03 18:16:07 -08001392 * @param moreChars characters to be added
1393 * @param explicit these characters were explicitly typed by the user, not pasted
Hans Boehm017de982015-06-10 17:46:03 -07001394 */
1395 private void addChars(String moreChars, boolean explicit) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001396 if (mUnprocessedChars != null) {
1397 moreChars = mUnprocessedChars + moreChars;
1398 }
1399 int current = 0;
1400 int len = moreChars.length();
Hans Boehm0b9806f2015-06-29 16:07:15 -07001401 boolean lastWasDigit = false;
Hans Boehm5d79d102015-09-16 16:33:47 -07001402 if (mCurrentState == CalculatorState.RESULT && len != 0) {
1403 // Clear display immediately for incomplete function name.
1404 switchToInput(KeyMaps.keyForChar(moreChars.charAt(current)));
1405 }
Hans Boehm24c91ed2016-06-30 18:53:44 -07001406 char groupingSeparator = KeyMaps.translateResult(",").charAt(0);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001407 while (current < len) {
1408 char c = moreChars.charAt(current);
Hans Boehm24c91ed2016-06-30 18:53:44 -07001409 if (Character.isSpaceChar(c) || c == groupingSeparator) {
1410 ++current;
1411 continue;
1412 }
Hans Boehm013969e2015-04-13 20:29:47 -07001413 int k = KeyMaps.keyForChar(c);
Hans Boehm0b9806f2015-06-29 16:07:15 -07001414 if (!explicit) {
1415 int expEnd;
1416 if (lastWasDigit && current !=
1417 (expEnd = Evaluator.exponentEnd(moreChars, current))) {
1418 // Process scientific notation with 'E' when pasting, in spite of ambiguity
1419 // with base of natural log.
1420 // Otherwise the 10^x key is the user's friend.
1421 mEvaluator.addExponent(moreChars, current, expEnd);
1422 current = expEnd;
1423 lastWasDigit = false;
1424 continue;
1425 } else {
1426 boolean isDigit = KeyMaps.digVal(k) != KeyMaps.NOT_DIGIT;
1427 if (current == 0 && (isDigit || k == R.id.dec_point)
Hans Boehm8f051c32016-10-03 16:53:58 -07001428 && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasTrailingConstant()) {
Hans Boehm0b9806f2015-06-29 16:07:15 -07001429 // Refuse to concatenate pasted content to trailing constant.
1430 // This makes pasting of calculator results more consistent, whether or
1431 // not the old calculator instance is still around.
1432 addKeyToExpr(R.id.op_mul);
1433 }
1434 lastWasDigit = (isDigit || lastWasDigit && k == R.id.dec_point);
1435 }
1436 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001437 if (k != View.NO_ID) {
1438 mCurrentButton = findViewById(k);
Hans Boehm017de982015-06-10 17:46:03 -07001439 if (explicit) {
1440 addExplicitKeyToExpr(k);
1441 } else {
1442 addKeyToExpr(k);
1443 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001444 if (Character.isSurrogate(c)) {
1445 current += 2;
1446 } else {
1447 ++current;
1448 }
1449 continue;
1450 }
Hans Boehm013969e2015-04-13 20:29:47 -07001451 int f = KeyMaps.funForString(moreChars, current);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001452 if (f != View.NO_ID) {
1453 mCurrentButton = findViewById(f);
Hans Boehm017de982015-06-10 17:46:03 -07001454 if (explicit) {
1455 addExplicitKeyToExpr(f);
1456 } else {
1457 addKeyToExpr(f);
1458 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001459 if (f == R.id.op_sqrt) {
1460 // Square root entered as function; don't lose the parenthesis.
1461 addKeyToExpr(R.id.lparen);
1462 }
1463 current = moreChars.indexOf('(', current) + 1;
1464 continue;
1465 }
1466 // There are characters left, but we can't convert them to button presses.
1467 mUnprocessedChars = moreChars.substring(current);
1468 redisplayAfterFormulaChange();
Hans Boehm52d477a2016-04-01 17:42:50 -07001469 showOrHideToolbar();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001470 return;
1471 }
1472 mUnprocessedChars = null;
1473 redisplayAfterFormulaChange();
Hans Boehm52d477a2016-04-01 17:42:50 -07001474 showOrHideToolbar();
Hans Boehm84614952014-11-25 18:46:17 -08001475 }
1476
Hans Boehm8f051c32016-10-03 16:53:58 -07001477 private void clearIfNotInputState() {
1478 if (mCurrentState == CalculatorState.ERROR
1479 || mCurrentState == CalculatorState.RESULT) {
1480 setState(CalculatorState.INPUT);
1481 mEvaluator.clearMain();
1482 }
1483 }
1484
Chenjie Yu3937b652016-06-01 23:14:26 -07001485 /**
Christine Franksbd90b792016-11-22 10:28:26 -08001486 * Since we only support LTR format, using the RTL comma does not make sense.
1487 */
1488 private String getDecimalSeparator() {
1489 final char defaultSeparator = DecimalFormatSymbols.getInstance().getDecimalSeparator();
1490 final char rtlComma = '\u066b';
1491 return defaultSeparator == rtlComma ? "," : String.valueOf(defaultSeparator);
1492 }
1493
1494 /**
Chenjie Yu3937b652016-06-01 23:14:26 -07001495 * Clean up animation for context menu.
1496 */
1497 @Override
1498 public void onContextMenuClosed(Menu menu) {
1499 stopActionModeOrContextMenu();
1500 }
Christine Franks1d99be12016-11-14 14:00:36 -08001501
1502 public interface OnDisplayMemoryOperationsListener {
1503 boolean shouldDisplayMemory();
1504 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001505}