blob: abf18d5bbc63a112f0713c59d6f1c5d75e6ca0aa [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;
Justin Klaassenfc5ac822015-06-18 13:15:17 -070036import android.content.ClipData;
Hans Boehm5e6a0ca2015-09-22 17:09:01 -070037import android.content.DialogInterface;
Justin Klaassend36d63e2015-05-05 12:59:36 -070038import android.content.Intent;
Hans Boehmbfe8c222015-04-02 16:26:07 -070039import android.content.res.Resources;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070040import android.graphics.Color;
Justin Klaassen8fff1442014-06-19 10:43:29 -070041import android.graphics.Rect;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070042import android.net.Uri;
Justin Klaassen4b3af052014-05-27 17:53:10 -070043import android.os.Bundle;
Justin Klaassenf79d6f62014-08-26 12:27:08 -070044import android.support.annotation.NonNull;
Justin Klaassen3b4d13d2014-06-06 18:18:37 +010045import android.support.v4.view.ViewPager;
Annie Chine918fd22016-03-09 11:07:54 -080046import android.text.Editable;
Hans Boehm8a4f81c2015-07-09 10:41:25 -070047import android.text.SpannableStringBuilder;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070048import android.text.Spanned;
Annie Chinf360ef02016-03-10 13:45:39 -080049import android.text.TextUtils;
Annie Chine918fd22016-03-09 11:07:54 -080050import android.text.TextWatcher;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070051import android.text.style.ForegroundColorSpan;
Justin Klaassen44595162015-05-28 17:55:20 -070052import android.util.Property;
Annie Chine918fd22016-03-09 11:07:54 -080053import android.view.ActionMode;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070054import android.view.KeyCharacterMap;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -070055import android.view.KeyEvent;
Hans Boehm84614952014-11-25 18:46:17 -080056import android.view.Menu;
57import android.view.MenuItem;
Justin Klaassen4b3af052014-05-27 17:53:10 -070058import android.view.View;
59import android.view.View.OnLongClickListener;
Justin Klaassen5f2a3342014-06-11 17:40:22 -070060import android.view.ViewAnimationUtils;
Justin Klaassen8fff1442014-06-19 10:43:29 -070061import android.view.ViewGroupOverlay;
Annie Chine918fd22016-03-09 11:07:54 -080062import android.view.ViewTreeObserver;
Justin Klaassen4b3af052014-05-27 17:53:10 -070063import android.view.animation.AccelerateDecelerateInterpolator;
Annie Chine918fd22016-03-09 11:07:54 -080064import android.widget.HorizontalScrollView;
Justin Klaassenfed941a2014-06-09 18:42:40 +010065import android.widget.TextView;
Justin Klaassend48b7562015-04-16 16:51:38 -070066import android.widget.Toolbar;
Justin Klaassenfed941a2014-06-09 18:42:40 +010067
Hans Boehm08e8f322015-04-21 13:18:38 -070068import com.android.calculator2.CalculatorText.OnTextSizeChangeListener;
Hans Boehm84614952014-11-25 18:46:17 -080069
70import java.io.ByteArrayInputStream;
Hans Boehm84614952014-11-25 18:46:17 -080071import java.io.ByteArrayOutputStream;
Hans Boehm84614952014-11-25 18:46:17 -080072import java.io.IOException;
Justin Klaassen721ec842015-05-28 14:30:08 -070073import java.io.ObjectInput;
74import java.io.ObjectInputStream;
75import java.io.ObjectOutput;
76import java.io.ObjectOutputStream;
Justin Klaassen4b3af052014-05-27 17:53:10 -070077
Justin Klaassen04f79c72014-06-27 17:25:35 -070078public class Calculator extends Activity
Hans Boehm5e6a0ca2015-09-22 17:09:01 -070079 implements OnTextSizeChangeListener, OnLongClickListener, CalculatorText.OnPasteListener,
80 AlertDialogFragment.OnClickListener {
Justin Klaassen2be4fdb2014-08-06 19:54:09 -070081
82 /**
83 * Constant for an invalid resource id.
84 */
85 public static final int INVALID_RES_ID = -1;
Justin Klaassen4b3af052014-05-27 17:53:10 -070086
87 private enum CalculatorState {
Hans Boehm84614952014-11-25 18:46:17 -080088 INPUT, // Result and formula both visible, no evaluation requested,
89 // Though result may be visible on bottom line.
90 EVALUATE, // Both visible, evaluation requested, evaluation/animation incomplete.
Hans Boehmc1ea0912015-06-19 15:05:07 -070091 // Not used for instant result evaluation.
Hans Boehm84614952014-11-25 18:46:17 -080092 INIT, // Very temporary state used as alternative to EVALUATE
93 // during reinitialization. Do not animate on completion.
94 ANIMATE, // Result computed, animation to enlarge result window in progress.
95 RESULT, // Result displayed, formula invisible.
96 // If we are in RESULT state, the formula was evaluated without
97 // error to initial precision.
98 ERROR // Error displayed: Formula visible, result shows error message.
99 // Display similar to INPUT state.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700100 }
Hans Boehm84614952014-11-25 18:46:17 -0800101 // Normal transition sequence is
102 // INPUT -> EVALUATE -> ANIMATE -> RESULT (or ERROR) -> INPUT
103 // A RESULT -> ERROR transition is possible in rare corner cases, in which
104 // a higher precision evaluation exposes an error. This is possible, since we
105 // initially evaluate assuming we were given a well-defined problem. If we
106 // were actually asked to compute sqrt(<extremely tiny negative number>) we produce 0
107 // unless we are asked for enough precision that we can distinguish the argument from zero.
108 // TODO: Consider further heuristics to reduce the chance of observing this?
109 // It already seems to be observable only in contrived cases.
110 // ANIMATE, ERROR, and RESULT are translated to an INIT state if the application
111 // is restarted in that state. This leads us to recompute and redisplay the result
112 // ASAP.
113 // TODO: Possibly save a bit more information, e.g. its initial display string
114 // or most significant digit position, to speed up restart.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700115
Justin Klaassen44595162015-05-28 17:55:20 -0700116 private final Property<TextView, Integer> TEXT_COLOR =
117 new Property<TextView, Integer>(Integer.class, "textColor") {
118 @Override
119 public Integer get(TextView textView) {
120 return textView.getCurrentTextColor();
121 }
122
123 @Override
124 public void set(TextView textView, Integer textColor) {
125 textView.setTextColor(textColor);
126 }
127 };
128
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800129 private static final String NAME = "Calculator";
Hans Boehm84614952014-11-25 18:46:17 -0800130 private static final String KEY_DISPLAY_STATE = NAME + "_display_state";
Hans Boehm760a9dc2015-04-20 10:27:12 -0700131 private static final String KEY_UNPROCESSED_CHARS = NAME + "_unprocessed_chars";
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800132 /**
133 * Associated value is a byte array holding the evaluator state.
134 */
Hans Boehm84614952014-11-25 18:46:17 -0800135 private static final String KEY_EVAL_STATE = NAME + "_eval_state";
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800136 private static final String KEY_INVERSE_MODE = NAME + "_inverse_mode";
Justin Klaassen741471e2014-06-11 09:43:44 -0700137
Annie Chine918fd22016-03-09 11:07:54 -0800138 private final ViewTreeObserver.OnPreDrawListener mPreDrawListener =
139 new ViewTreeObserver.OnPreDrawListener() {
140 @Override
141 public boolean onPreDraw() {
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700142 mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
Annie Chine918fd22016-03-09 11:07:54 -0800143 final ViewTreeObserver observer = mFormulaContainer.getViewTreeObserver();
144 if (observer.isAlive()) {
145 observer.removeOnPreDrawListener(this);
146 }
147 return false;
148 }
149 };
150
151 private final TextWatcher mFormulaTextWatcher = new TextWatcher() {
152 @Override
153 public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
154 }
155
156 @Override
157 public void onTextChanged(CharSequence charSequence, int start, int count, int after) {
158 }
159
160 @Override
161 public void afterTextChanged(Editable editable) {
162 final ViewTreeObserver observer = mFormulaContainer.getViewTreeObserver();
163 if (observer.isAlive()) {
164 observer.removeOnPreDrawListener(mPreDrawListener);
165 observer.addOnPreDrawListener(mPreDrawListener);
166 }
167 }
168 };
169
Justin Klaassen4b3af052014-05-27 17:53:10 -0700170 private CalculatorState mCurrentState;
Hans Boehm84614952014-11-25 18:46:17 -0800171 private Evaluator mEvaluator;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700172
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800173 private CalculatorDisplay mDisplayView;
Justin Klaassend48b7562015-04-16 16:51:38 -0700174 private TextView mModeView;
Hans Boehm08e8f322015-04-21 13:18:38 -0700175 private CalculatorText mFormulaText;
Justin Klaassen44595162015-05-28 17:55:20 -0700176 private CalculatorResult mResultText;
Annie Chine918fd22016-03-09 11:07:54 -0800177 private HorizontalScrollView mFormulaContainer;
Justin Klaassend48b7562015-04-16 16:51:38 -0700178
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100179 private ViewPager mPadViewPager;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700180 private View mDeleteButton;
181 private View mClearButton;
Justin Klaassend48b7562015-04-16 16:51:38 -0700182 private View mEqualButton;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700183
184 private TextView mInverseToggle;
185 private TextView mModeToggle;
186
Justin Klaassen721ec842015-05-28 14:30:08 -0700187 private View[] mInvertibleButtons;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700188 private View[] mInverseButtons;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700189
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700190 private View mCurrentButton;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700191 private Animator mCurrentAnimator;
192
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700193 // Characters that were recently entered at the end of the display that have not yet
194 // been added to the underlying expression.
195 private String mUnprocessedChars = null;
196
197 // Color to highlight unprocessed characters from physical keyboard.
198 // TODO: should probably match this to the error color?
199 private ForegroundColorSpan mUnprocessedColorSpan = new ForegroundColorSpan(Color.RED);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700200
Annie Chin26e159e2016-05-18 15:17:14 -0700201 // Whether the display is one line.
202 private boolean mOneLine;
203
Justin Klaassen4b3af052014-05-27 17:53:10 -0700204 @Override
205 protected void onCreate(Bundle savedInstanceState) {
206 super.onCreate(savedInstanceState);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700207 setContentView(R.layout.activity_calculator);
Justin Klaassend48b7562015-04-16 16:51:38 -0700208 setActionBar((Toolbar) findViewById(R.id.toolbar));
209
210 // Hide all default options in the ActionBar.
211 getActionBar().setDisplayOptions(0);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700212
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800213 // Ensure the toolbar stays visible while the options menu is displayed.
214 getActionBar().addOnMenuVisibilityListener(new ActionBar.OnMenuVisibilityListener() {
215 @Override
216 public void onMenuVisibilityChanged(boolean isVisible) {
217 mDisplayView.setForceToolbarVisible(isVisible);
218 }
219 });
220
221 mDisplayView = (CalculatorDisplay) findViewById(R.id.display);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700222 mModeView = (TextView) findViewById(R.id.mode);
Hans Boehm08e8f322015-04-21 13:18:38 -0700223 mFormulaText = (CalculatorText) findViewById(R.id.formula);
Justin Klaassen44595162015-05-28 17:55:20 -0700224 mResultText = (CalculatorResult) findViewById(R.id.result);
Annie Chine918fd22016-03-09 11:07:54 -0800225 mFormulaContainer = (HorizontalScrollView) findViewById(R.id.formula_container);
Justin Klaassend48b7562015-04-16 16:51:38 -0700226
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100227 mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700228 mDeleteButton = findViewById(R.id.del);
229 mClearButton = findViewById(R.id.clr);
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700230 mEqualButton = findViewById(R.id.pad_numeric).findViewById(R.id.eq);
231 if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) {
232 mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq);
233 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700234
235 mInverseToggle = (TextView) findViewById(R.id.toggle_inv);
236 mModeToggle = (TextView) findViewById(R.id.toggle_mode);
237
Annie Chin26e159e2016-05-18 15:17:14 -0700238 mOneLine = mResultText.getVisibility() == View.INVISIBLE;
239
Justin Klaassen721ec842015-05-28 14:30:08 -0700240 mInvertibleButtons = new View[] {
241 findViewById(R.id.fun_sin),
242 findViewById(R.id.fun_cos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700243 findViewById(R.id.fun_tan),
244 findViewById(R.id.fun_ln),
245 findViewById(R.id.fun_log),
246 findViewById(R.id.op_sqrt)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700247 };
248 mInverseButtons = new View[] {
249 findViewById(R.id.fun_arcsin),
250 findViewById(R.id.fun_arccos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700251 findViewById(R.id.fun_arctan),
252 findViewById(R.id.fun_exp),
253 findViewById(R.id.fun_10pow),
254 findViewById(R.id.op_sqr)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700255 };
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700256
Justin Klaassen44595162015-05-28 17:55:20 -0700257 mEvaluator = new Evaluator(this, mResultText);
258 mResultText.setEvaluator(mEvaluator);
Hans Boehm013969e2015-04-13 20:29:47 -0700259 KeyMaps.setActivity(this);
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700260
Hans Boehm84614952014-11-25 18:46:17 -0800261 if (savedInstanceState != null) {
262 setState(CalculatorState.values()[
263 savedInstanceState.getInt(KEY_DISPLAY_STATE,
264 CalculatorState.INPUT.ordinal())]);
Hans Boehm760a9dc2015-04-20 10:27:12 -0700265 CharSequence unprocessed = savedInstanceState.getCharSequence(KEY_UNPROCESSED_CHARS);
266 if (unprocessed != null) {
267 mUnprocessedChars = unprocessed.toString();
268 }
269 byte[] state = savedInstanceState.getByteArray(KEY_EVAL_STATE);
Hans Boehm84614952014-11-25 18:46:17 -0800270 if (state != null) {
271 try (ObjectInput in = new ObjectInputStream(new ByteArrayInputStream(state))) {
272 mEvaluator.restoreInstanceState(in);
273 } catch (Throwable ignored) {
274 // When in doubt, revert to clean state
275 mCurrentState = CalculatorState.INPUT;
276 mEvaluator.clear();
277 }
278 }
Hans Boehmfbcef702015-04-27 18:07:47 -0700279 } else {
280 mCurrentState = CalculatorState.INPUT;
281 mEvaluator.clear();
Hans Boehm84614952014-11-25 18:46:17 -0800282 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700283
Hans Boehm08e8f322015-04-21 13:18:38 -0700284 mFormulaText.setOnTextSizeChangeListener(this);
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700285 mFormulaText.setOnPasteListener(this);
Annie Chine918fd22016-03-09 11:07:54 -0800286 mFormulaText.addTextChangedListener(mFormulaTextWatcher);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700287 mDeleteButton.setOnLongClickListener(this);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700288
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800289 onInverseToggled(savedInstanceState != null
290 && savedInstanceState.getBoolean(KEY_INVERSE_MODE));
Justin Klaassene2711cb2015-05-28 11:13:17 -0700291 onModeChanged(mEvaluator.getDegreeMode());
292
Hans Boehm84614952014-11-25 18:46:17 -0800293 if (mCurrentState != CalculatorState.INPUT) {
Hans Boehmfbcef702015-04-27 18:07:47 -0700294 // Just reevaluate.
295 redisplayFormula();
Hans Boehm84614952014-11-25 18:46:17 -0800296 setState(CalculatorState.INIT);
Hans Boehm84614952014-11-25 18:46:17 -0800297 mEvaluator.requireResult();
298 } else {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700299 redisplayAfterFormulaChange();
Hans Boehm84614952014-11-25 18:46:17 -0800300 }
301 // TODO: We're currently not saving and restoring scroll position.
302 // We probably should. Details may require care to deal with:
303 // - new display size
304 // - slow recomputation if we've scrolled far.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700305 }
306
307 @Override
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800308 protected void onResume() {
309 super.onResume();
310
Hans Boehm52d477a2016-04-01 17:42:50 -0700311 // Always temporarily show the toolbar initially on launch.
312 showAndMaybeHideToolbar();
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800313 }
314
315 @Override
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700316 protected void onSaveInstanceState(@NonNull Bundle outState) {
Hans Boehm40125442016-01-22 10:35:35 -0800317 mEvaluator.cancelAll(true);
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700318 // If there's an animation in progress, cancel it first to ensure our state is up-to-date.
319 if (mCurrentAnimator != null) {
320 mCurrentAnimator.cancel();
321 }
322
Justin Klaassen4b3af052014-05-27 17:53:10 -0700323 super.onSaveInstanceState(outState);
Hans Boehm84614952014-11-25 18:46:17 -0800324 outState.putInt(KEY_DISPLAY_STATE, mCurrentState.ordinal());
Hans Boehm760a9dc2015-04-20 10:27:12 -0700325 outState.putCharSequence(KEY_UNPROCESSED_CHARS, mUnprocessedChars);
Hans Boehm84614952014-11-25 18:46:17 -0800326 ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
327 try (ObjectOutput out = new ObjectOutputStream(byteArrayStream)) {
328 mEvaluator.saveInstanceState(out);
329 } catch (IOException e) {
330 // Impossible; No IO involved.
331 throw new AssertionError("Impossible IO exception", e);
332 }
333 outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray());
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800334 outState.putBoolean(KEY_INVERSE_MODE, mInverseToggle.isSelected());
Justin Klaassen4b3af052014-05-27 17:53:10 -0700335 }
336
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700337 // Set the state, updating delete label and display colors.
338 // This restores display positions on moving to INPUT.
Justin Klaassend48b7562015-04-16 16:51:38 -0700339 // But movement/animation for moving to RESULT has already been done.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700340 private void setState(CalculatorState state) {
341 if (mCurrentState != state) {
Hans Boehm84614952014-11-25 18:46:17 -0800342 if (state == CalculatorState.INPUT) {
343 restoreDisplayPositions();
344 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700345 mCurrentState = state;
346
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700347 if (mCurrentState == CalculatorState.RESULT) {
348 // No longer do this for ERROR; allow mistakes to be corrected.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700349 mDeleteButton.setVisibility(View.GONE);
350 mClearButton.setVisibility(View.VISIBLE);
351 } else {
352 mDeleteButton.setVisibility(View.VISIBLE);
353 mClearButton.setVisibility(View.GONE);
354 }
355
Annie Chin26e159e2016-05-18 15:17:14 -0700356 if (mOneLine) {
357 if (mCurrentState == CalculatorState.RESULT
358 || mCurrentState == CalculatorState.EVALUATE
359 || mCurrentState == CalculatorState.ANIMATE) {
360 mFormulaText.setVisibility(View.VISIBLE);
361 mResultText.setVisibility(View.VISIBLE);
Annie Chin947d93b2016-06-14 10:18:54 -0700362 } else if (mCurrentState == CalculatorState.ERROR) {
363 mFormulaText.setVisibility(View.INVISIBLE);
364 mResultText.setVisibility(View.VISIBLE);
Annie Chin26e159e2016-05-18 15:17:14 -0700365 } else {
366 mFormulaText.setVisibility(View.VISIBLE);
367 mResultText.setVisibility(View.INVISIBLE);
368 }
369 }
370
Hans Boehm84614952014-11-25 18:46:17 -0800371 if (mCurrentState == CalculatorState.ERROR) {
Justin Klaassen44595162015-05-28 17:55:20 -0700372 final int errorColor = getColor(R.color.calculator_error_color);
Hans Boehm08e8f322015-04-21 13:18:38 -0700373 mFormulaText.setTextColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700374 mResultText.setTextColor(errorColor);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700375 getWindow().setStatusBarColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700376 } else if (mCurrentState != CalculatorState.RESULT) {
377 mFormulaText.setTextColor(getColor(R.color.display_formula_text_color));
378 mResultText.setTextColor(getColor(R.color.display_result_text_color));
379 getWindow().setStatusBarColor(getColor(R.color.calculator_accent_color));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700380 }
Justin Klaassend48b7562015-04-16 16:51:38 -0700381
382 invalidateOptionsMenu();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700383 }
384 }
385
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700386 @Override
387 public void onActionModeStarted(ActionMode mode) {
388 super.onActionModeStarted(mode);
389 if (mode.getTag() == CalculatorText.TAG_ACTION_MODE) {
390 mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
391 }
392 }
393
Hans Boehm1176f232015-05-11 16:26:03 -0700394 // Stop any active ActionMode. Return true if there was one.
395 private boolean stopActionMode() {
Justin Klaassen44595162015-05-28 17:55:20 -0700396 if (mResultText.stopActionMode()) {
Hans Boehm1176f232015-05-11 16:26:03 -0700397 return true;
398 }
399 if (mFormulaText.stopActionMode()) {
400 return true;
401 }
402 return false;
403 }
404
Justin Klaassen4b3af052014-05-27 17:53:10 -0700405 @Override
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700406 public void onUserInteraction() {
407 super.onUserInteraction();
408
409 // If there's an animation in progress, end it immediately, so the user interaction can
410 // be handled.
411 if (mCurrentAnimator != null) {
412 mCurrentAnimator.end();
413 }
414 }
415
416 @Override
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100417 public void onBackPressed() {
Hans Boehm1176f232015-05-11 16:26:03 -0700418 if (!stopActionMode()) {
419 if (mPadViewPager != null && mPadViewPager.getCurrentItem() != 0) {
420 // Select the previous pad.
421 mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
422 } else {
423 // If the user is currently looking at the first pad (or the pad is not paged),
424 // allow the system to handle the Back button.
425 super.onBackPressed();
426 }
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100427 }
428 }
429
430 @Override
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700431 public boolean onKeyUp(int keyCode, KeyEvent event) {
Justin Klaassen83959da2016-04-06 11:55:24 -0700432 // Allow the system to handle special key codes (e.g. "BACK" or "DPAD").
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700433 switch (keyCode) {
Justin Klaassen83959da2016-04-06 11:55:24 -0700434 case KeyEvent.KEYCODE_BACK:
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700435 case KeyEvent.KEYCODE_DPAD_UP:
436 case KeyEvent.KEYCODE_DPAD_DOWN:
437 case KeyEvent.KEYCODE_DPAD_LEFT:
438 case KeyEvent.KEYCODE_DPAD_RIGHT:
439 return super.onKeyUp(keyCode, event);
440 }
441
Justin Klaassend12e0622016-04-27 16:26:47 -0700442 // Stop the action mode if it's showing.
443 stopActionMode();
444
Justin Klaassen12da1ad2016-04-04 14:20:37 -0700445 // Always cancel unrequested in-progress evaluation, so that we don't have to worry about
446 // subsequent asynchronous completion.
447 // Requested in-progress evaluations are handled below.
448 if (mCurrentState != CalculatorState.EVALUATE) {
449 mEvaluator.cancelAll(true);
450 }
451
452 switch (keyCode) {
453 case KeyEvent.KEYCODE_NUMPAD_ENTER:
454 case KeyEvent.KEYCODE_ENTER:
455 case KeyEvent.KEYCODE_DPAD_CENTER:
456 mCurrentButton = mEqualButton;
457 onEquals();
458 return true;
459 case KeyEvent.KEYCODE_DEL:
460 mCurrentButton = mDeleteButton;
461 onDelete();
462 return true;
463 default:
464 cancelIfEvaluating(false);
465 final int raw = event.getKeyCharacterMap().get(keyCode, event.getMetaState());
466 if ((raw & KeyCharacterMap.COMBINING_ACCENT) != 0) {
467 return true; // discard
468 }
469 // Try to discard non-printing characters and the like.
470 // The user will have to explicitly delete other junk that gets past us.
471 if (Character.isIdentifierIgnorable(raw) || Character.isWhitespace(raw)) {
472 return true;
473 }
474 char c = (char) raw;
475 if (c == '=') {
476 mCurrentButton = mEqualButton;
477 onEquals();
478 } else {
479 addChars(String.valueOf(c), true);
480 redisplayAfterFormulaChange();
481 }
482 return true;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700483 }
484 }
485
Justin Klaassene2711cb2015-05-28 11:13:17 -0700486 /**
487 * Invoked whenever the inverse button is toggled to update the UI.
488 *
489 * @param showInverse {@code true} if inverse functions should be shown
490 */
491 private void onInverseToggled(boolean showInverse) {
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800492 mInverseToggle.setSelected(showInverse);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700493 if (showInverse) {
494 mInverseToggle.setContentDescription(getString(R.string.desc_inv_on));
Justin Klaassen721ec842015-05-28 14:30:08 -0700495 for (View invertibleButton : mInvertibleButtons) {
496 invertibleButton.setVisibility(View.GONE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700497 }
498 for (View inverseButton : mInverseButtons) {
499 inverseButton.setVisibility(View.VISIBLE);
500 }
501 } else {
502 mInverseToggle.setContentDescription(getString(R.string.desc_inv_off));
Justin Klaassen721ec842015-05-28 14:30:08 -0700503 for (View invertibleButton : mInvertibleButtons) {
504 invertibleButton.setVisibility(View.VISIBLE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700505 }
506 for (View inverseButton : mInverseButtons) {
507 inverseButton.setVisibility(View.GONE);
508 }
509 }
510 }
511
512 /**
513 * Invoked whenever the deg/rad mode may have changed to update the UI.
514 *
515 * @param degreeMode {@code true} if in degree mode
516 */
517 private void onModeChanged(boolean degreeMode) {
518 if (degreeMode) {
Justin Klaassend48b7562015-04-16 16:51:38 -0700519 mModeView.setText(R.string.mode_deg);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700520 mModeView.setContentDescription(getString(R.string.desc_mode_deg));
521
522 mModeToggle.setText(R.string.mode_rad);
523 mModeToggle.setContentDescription(getString(R.string.desc_switch_rad));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700524 } else {
Justin Klaassend48b7562015-04-16 16:51:38 -0700525 mModeView.setText(R.string.mode_rad);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700526 mModeView.setContentDescription(getString(R.string.desc_mode_rad));
527
528 mModeToggle.setText(R.string.mode_deg);
529 mModeToggle.setContentDescription(getString(R.string.desc_switch_deg));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700530 }
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800531
532 // Show the toolbar to highlight the mode change.
Hans Boehm52d477a2016-04-01 17:42:50 -0700533 showAndMaybeHideToolbar();
Hans Boehmbfe8c222015-04-02 16:26:07 -0700534 }
Hans Boehm84614952014-11-25 18:46:17 -0800535
Hans Boehm5d79d102015-09-16 16:33:47 -0700536 /**
537 * Switch to INPUT from RESULT state in response to input of the specified button_id.
538 * View.NO_ID is treated as an incomplete function id.
539 */
540 private void switchToInput(int button_id) {
541 if (KeyMaps.isBinary(button_id) || KeyMaps.isSuffix(button_id)) {
542 mEvaluator.collapse();
543 } else {
544 announceClearedForAccessibility();
545 mEvaluator.clear();
546 }
547 setState(CalculatorState.INPUT);
548 }
549
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700550 // Add the given button id to input expression.
551 // If appropriate, clear the expression before doing so.
552 private void addKeyToExpr(int id) {
553 if (mCurrentState == CalculatorState.ERROR) {
554 setState(CalculatorState.INPUT);
555 } else if (mCurrentState == CalculatorState.RESULT) {
Hans Boehm5d79d102015-09-16 16:33:47 -0700556 switchToInput(id);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700557 }
558 if (!mEvaluator.append(id)) {
559 // TODO: Some user visible feedback?
560 }
561 }
562
Hans Boehm017de982015-06-10 17:46:03 -0700563 /**
564 * Add the given button id to input expression, assuming it was explicitly
565 * typed/touched.
566 * We perform slightly more aggressive correction than in pasted expressions.
567 */
568 private void addExplicitKeyToExpr(int id) {
569 if (mCurrentState == CalculatorState.INPUT && id == R.id.op_sub) {
570 mEvaluator.getExpr().removeTrailingAdditiveOperators();
571 }
572 addKeyToExpr(id);
573 }
574
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700575 private void redisplayAfterFormulaChange() {
576 // TODO: Could do this more incrementally.
577 redisplayFormula();
578 setState(CalculatorState.INPUT);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800579 if (haveUnprocessed()) {
Justin Klaassen44595162015-05-28 17:55:20 -0700580 mResultText.clear();
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800581 // Force reevaluation when text is deleted, even if expression is unchanged.
582 mEvaluator.touch();
583 } else {
584 if (mEvaluator.getExpr().hasInterestingOps()) {
585 mEvaluator.evaluateAndShowResult();
586 } else {
587 mResultText.clear();
588 }
Hans Boehmc023b732015-04-29 11:30:47 -0700589 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700590 }
591
Hans Boehm52d477a2016-04-01 17:42:50 -0700592 /**
593 * Show the toolbar.
594 * Automatically hide it again if it's not relevant to current formula.
595 */
596 private void showAndMaybeHideToolbar() {
597 final boolean shouldBeVisible =
598 mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
599 mDisplayView.showToolbar(!shouldBeVisible);
600 }
601
602 /**
603 * Display or hide the toolbar depending on calculator state.
604 */
605 private void showOrHideToolbar() {
606 final boolean shouldBeVisible =
607 mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
608 if (shouldBeVisible) {
609 mDisplayView.showToolbar(false);
610 } else {
611 mDisplayView.hideToolbar();
612 }
613 }
614
Justin Klaassen4b3af052014-05-27 17:53:10 -0700615 public void onButtonClick(View view) {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700616 // Any animation is ended before we get here.
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700617 mCurrentButton = view;
Hans Boehm1176f232015-05-11 16:26:03 -0700618 stopActionMode();
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800619
Hans Boehmc1ea0912015-06-19 15:05:07 -0700620 // See onKey above for the rationale behind some of the behavior below:
621 if (mCurrentState != CalculatorState.EVALUATE) {
622 // Cancel evaluations that were not specifically requested.
623 mEvaluator.cancelAll(true);
Hans Boehm84614952014-11-25 18:46:17 -0800624 }
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800625
Justin Klaassend48b7562015-04-16 16:51:38 -0700626 final int id = view.getId();
Hans Boehm84614952014-11-25 18:46:17 -0800627 switch (id) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700628 case R.id.eq:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700629 onEquals();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700630 break;
631 case R.id.del:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700632 onDelete();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700633 break;
634 case R.id.clr:
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700635 onClear();
Hans Boehm52d477a2016-04-01 17:42:50 -0700636 return; // Toolbar visibility adjusted at end of animation.
Justin Klaassene2711cb2015-05-28 11:13:17 -0700637 case R.id.toggle_inv:
638 final boolean selected = !mInverseToggle.isSelected();
639 mInverseToggle.setSelected(selected);
640 onInverseToggled(selected);
Hans Boehmc1ea0912015-06-19 15:05:07 -0700641 if (mCurrentState == CalculatorState.RESULT) {
642 mResultText.redisplay(); // In case we cancelled reevaluation.
643 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700644 break;
645 case R.id.toggle_mode:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700646 cancelIfEvaluating(false);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700647 final boolean mode = !mEvaluator.getDegreeMode();
Hans Boehmbfe8c222015-04-02 16:26:07 -0700648 if (mCurrentState == CalculatorState.RESULT) {
649 mEvaluator.collapse(); // Capture result evaluated in old mode
650 redisplayFormula();
651 }
652 // In input mode, we reinterpret already entered trig functions.
653 mEvaluator.setDegreeMode(mode);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700654 onModeChanged(mode);
Hans Boehmbfe8c222015-04-02 16:26:07 -0700655 setState(CalculatorState.INPUT);
Justin Klaassen44595162015-05-28 17:55:20 -0700656 mResultText.clear();
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800657 if (!haveUnprocessed() && mEvaluator.getExpr().hasInterestingOps()) {
Hans Boehmc023b732015-04-29 11:30:47 -0700658 mEvaluator.evaluateAndShowResult();
659 }
Hans Boehm52d477a2016-04-01 17:42:50 -0700660 return; // onModeChanged adjusted toolbar visibility.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700661 default:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700662 cancelIfEvaluating(false);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800663 if (haveUnprocessed()) {
664 // For consistency, append as uninterpreted characters.
665 // This may actually be useful for a left parenthesis.
666 addChars(KeyMaps.toString(this, id), true);
667 } else {
668 addExplicitKeyToExpr(id);
669 redisplayAfterFormulaChange();
670 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700671 break;
672 }
Hans Boehm52d477a2016-04-01 17:42:50 -0700673 showOrHideToolbar();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700674 }
675
Hans Boehm84614952014-11-25 18:46:17 -0800676 void redisplayFormula() {
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700677 SpannableStringBuilder formula = mEvaluator.getExpr().toSpannableStringBuilder(this);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700678 if (mUnprocessedChars != null) {
679 // Add and highlight characters we couldn't process.
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700680 formula.append(mUnprocessedChars, mUnprocessedColorSpan,
681 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700682 }
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700683 mFormulaText.changeTextTo(formula);
Annie Chinf360ef02016-03-10 13:45:39 -0800684 mFormulaText.setContentDescription(TextUtils.isEmpty(formula)
685 ? getString(R.string.desc_formula) : formula.toString());
Hans Boehm84614952014-11-25 18:46:17 -0800686 }
687
Justin Klaassen4b3af052014-05-27 17:53:10 -0700688 @Override
689 public boolean onLongClick(View view) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700690 mCurrentButton = view;
691
Justin Klaassen4b3af052014-05-27 17:53:10 -0700692 if (view.getId() == R.id.del) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700693 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700694 return true;
695 }
696 return false;
697 }
698
Hans Boehm84614952014-11-25 18:46:17 -0800699 // Initial evaluation completed successfully. Initiate display.
Hans Boehma0e45f32015-05-30 13:20:35 -0700700 public void onEvaluate(int initDisplayPrec, int msd, int leastDigPos,
701 String truncatedWholeNumber) {
Justin Klaassend48b7562015-04-16 16:51:38 -0700702 // Invalidate any options that may depend on the current result.
703 invalidateOptionsMenu();
704
Hans Boehma0e45f32015-05-30 13:20:35 -0700705 mResultText.displayResult(initDisplayPrec, msd, leastDigPos, truncatedWholeNumber);
Hans Boehm61568a12015-05-18 18:25:41 -0700706 if (mCurrentState != CalculatorState.INPUT) { // in EVALUATE or INIT state
Hans Boehm84614952014-11-25 18:46:17 -0800707 onResult(mCurrentState != CalculatorState.INIT);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700708 }
Hans Boehm84614952014-11-25 18:46:17 -0800709 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700710
Hans Boehmc1ea0912015-06-19 15:05:07 -0700711 // Reset state to reflect evaluator cancellation. Invoked by evaluator.
Hans Boehm84614952014-11-25 18:46:17 -0800712 public void onCancelled() {
713 // We should be in EVALUATE state.
Hans Boehm84614952014-11-25 18:46:17 -0800714 setState(CalculatorState.INPUT);
Justin Klaassen44595162015-05-28 17:55:20 -0700715 mResultText.clear();
Hans Boehm84614952014-11-25 18:46:17 -0800716 }
717
718 // Reevaluation completed; ask result to redisplay current value.
719 public void onReevaluate()
720 {
Justin Klaassen44595162015-05-28 17:55:20 -0700721 mResultText.redisplay();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700722 }
723
Justin Klaassenfed941a2014-06-09 18:42:40 +0100724 @Override
725 public void onTextSizeChanged(final TextView textView, float oldSize) {
726 if (mCurrentState != CalculatorState.INPUT) {
727 // Only animate text changes that occur from user input.
728 return;
729 }
730
731 // Calculate the values needed to perform the scale and translation animations,
732 // maintaining the same apparent baseline for the displayed text.
733 final float textScale = oldSize / textView.getTextSize();
734 final float translationX = (1.0f - textScale) *
735 (textView.getWidth() / 2.0f - textView.getPaddingEnd());
736 final float translationY = (1.0f - textScale) *
737 (textView.getHeight() / 2.0f - textView.getPaddingBottom());
738
739 final AnimatorSet animatorSet = new AnimatorSet();
740 animatorSet.playTogether(
741 ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f),
742 ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f),
743 ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f),
744 ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f));
Justin Klaassen94db7202014-06-11 11:22:31 -0700745 animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassenfed941a2014-06-09 18:42:40 +0100746 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
747 animatorSet.start();
748 }
749
Hans Boehmc1ea0912015-06-19 15:05:07 -0700750 /**
751 * Cancel any in-progress explicitly requested evaluations.
752 * @param quiet suppress pop-up message. Explicit evaluation can change the expression
753 value, and certainly changes the display, so it seems reasonable to warn.
754 * @return true if there was such an evaluation
755 */
756 private boolean cancelIfEvaluating(boolean quiet) {
757 if (mCurrentState == CalculatorState.EVALUATE) {
758 mEvaluator.cancelAll(quiet);
759 return true;
760 } else {
761 return false;
762 }
763 }
764
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800765 private boolean haveUnprocessed() {
766 return mUnprocessedChars != null && !mUnprocessedChars.isEmpty();
767 }
768
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700769 private void onEquals() {
Hans Boehm56d6e762016-06-06 11:46:29 -0700770 // Ignore if in non-INPUT state, or if there are no operators.
771 if (mCurrentState == CalculatorState.INPUT && mEvaluator.getExpr().hasInterestingOps()) {
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700772 setState(CalculatorState.EVALUATE);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800773 if (haveUnprocessed()) {
774 onError(R.string.error_syntax);
775 } else {
776 mEvaluator.requireResult();
777 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700778 }
779 }
780
781 private void onDelete() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700782 // Delete works like backspace; remove the last character or operator from the expression.
783 // Note that we handle keyboard delete exactly like the delete button. For
784 // example the delete button can be used to delete a character from an incomplete
785 // function name typed on a physical keyboard.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700786 // This should be impossible in RESULT state.
Hans Boehmc1ea0912015-06-19 15:05:07 -0700787 // If there is an in-progress explicit evaluation, just cancel it and return.
788 if (cancelIfEvaluating(false)) return;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700789 setState(CalculatorState.INPUT);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800790 if (haveUnprocessed()) {
791 mUnprocessedChars = mUnprocessedChars.substring(0, mUnprocessedChars.length() - 1);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700792 } else {
Hans Boehmc023b732015-04-29 11:30:47 -0700793 mEvaluator.delete();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700794 }
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800795 if (mEvaluator.getExpr().isEmpty() && !haveUnprocessed()) {
Hans Boehmdb6f9992015-08-19 12:32:56 -0700796 // Resulting formula won't be announced, since it's empty.
797 announceClearedForAccessibility();
798 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700799 redisplayAfterFormulaChange();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700800 }
801
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700802 private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
Justin Klaassen06360f92014-08-28 11:08:44 -0700803 final ViewGroupOverlay groupOverlay =
804 (ViewGroupOverlay) getWindow().getDecorView().getOverlay();
Justin Klaassen8fff1442014-06-19 10:43:29 -0700805
806 final Rect displayRect = new Rect();
Justin Klaassen06360f92014-08-28 11:08:44 -0700807 mDisplayView.getGlobalVisibleRect(displayRect);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700808
809 // Make reveal cover the display and status bar.
810 final View revealView = new View(this);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700811 revealView.setBottom(displayRect.bottom);
812 revealView.setLeft(displayRect.left);
813 revealView.setRight(displayRect.right);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800814 revealView.setBackgroundColor(getColor(colorRes));
Justin Klaassen06360f92014-08-28 11:08:44 -0700815 groupOverlay.add(revealView);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700816
Justin Klaassen4b3af052014-05-27 17:53:10 -0700817 final int[] clearLocation = new int[2];
818 sourceView.getLocationInWindow(clearLocation);
819 clearLocation[0] += sourceView.getWidth() / 2;
820 clearLocation[1] += sourceView.getHeight() / 2;
821
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700822 final int revealCenterX = clearLocation[0] - revealView.getLeft();
823 final int revealCenterY = clearLocation[1] - revealView.getTop();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700824
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700825 final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2);
826 final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2);
827 final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700828 final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2));
829
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700830 final Animator revealAnimator =
831 ViewAnimationUtils.createCircularReveal(revealView,
ztenghui3d6ecaf2014-06-05 09:56:00 -0700832 revealCenterX, revealCenterY, 0.0f, revealRadius);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700833 revealAnimator.setDuration(
Justin Klaassen4b3af052014-05-27 17:53:10 -0700834 getResources().getInteger(android.R.integer.config_longAnimTime));
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700835 revealAnimator.addListener(listener);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700836
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700837 final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700838 alphaAnimator.setDuration(
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700839 getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700840
841 final AnimatorSet animatorSet = new AnimatorSet();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700842 animatorSet.play(revealAnimator).before(alphaAnimator);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700843 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
844 animatorSet.addListener(new AnimatorListenerAdapter() {
845 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -0700846 public void onAnimationEnd(Animator animator) {
Justin Klaassen8fff1442014-06-19 10:43:29 -0700847 groupOverlay.remove(revealView);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700848 mCurrentAnimator = null;
849 }
850 });
851
852 mCurrentAnimator = animatorSet;
853 animatorSet.start();
854 }
855
Hans Boehmdb6f9992015-08-19 12:32:56 -0700856 private void announceClearedForAccessibility() {
857 mResultText.announceForAccessibility(getResources().getString(R.string.cleared));
Hans Boehmccc55662015-07-07 14:16:59 -0700858 }
859
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700860 private void onClear() {
Justin Klaassen1a428cf2016-02-24 15:58:18 -0800861 if (mEvaluator.getExpr().isEmpty() && !haveUnprocessed()) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700862 return;
863 }
Hans Boehmc1ea0912015-06-19 15:05:07 -0700864 cancelIfEvaluating(true);
Hans Boehmdb6f9992015-08-19 12:32:56 -0700865 announceClearedForAccessibility();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700866 reveal(mCurrentButton, R.color.calculator_accent_color, new AnimatorListenerAdapter() {
867 @Override
868 public void onAnimationEnd(Animator animation) {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700869 mUnprocessedChars = null;
Justin Klaassen44595162015-05-28 17:55:20 -0700870 mResultText.clear();
Hans Boehm760a9dc2015-04-20 10:27:12 -0700871 mEvaluator.clear();
872 setState(CalculatorState.INPUT);
Hans Boehm52d477a2016-04-01 17:42:50 -0700873 showOrHideToolbar();
Hans Boehm84614952014-11-25 18:46:17 -0800874 redisplayFormula();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700875 }
876 });
877 }
878
Hans Boehm84614952014-11-25 18:46:17 -0800879 // Evaluation encountered en error. Display the error.
880 void onError(final int errorResourceId) {
Hans Boehmfbcef702015-04-27 18:07:47 -0700881 if (mCurrentState == CalculatorState.EVALUATE) {
882 setState(CalculatorState.ANIMATE);
Hans Boehmccc55662015-07-07 14:16:59 -0700883 mResultText.announceForAccessibility(getResources().getString(errorResourceId));
Hans Boehmfbcef702015-04-27 18:07:47 -0700884 reveal(mCurrentButton, R.color.calculator_error_color,
885 new AnimatorListenerAdapter() {
886 @Override
887 public void onAnimationEnd(Animator animation) {
888 setState(CalculatorState.ERROR);
Justin Klaassen44595162015-05-28 17:55:20 -0700889 mResultText.displayError(errorResourceId);
Hans Boehmfbcef702015-04-27 18:07:47 -0700890 }
891 });
892 } else if (mCurrentState == CalculatorState.INIT) {
893 setState(CalculatorState.ERROR);
Justin Klaassen44595162015-05-28 17:55:20 -0700894 mResultText.displayError(errorResourceId);
Hans Boehmc023b732015-04-29 11:30:47 -0700895 } else {
Justin Klaassen44595162015-05-28 17:55:20 -0700896 mResultText.clear();
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700897 }
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700898 }
899
Hans Boehm84614952014-11-25 18:46:17 -0800900 // Animate movement of result into the top formula slot.
901 // Result window now remains translated in the top slot while the result is displayed.
902 // (We convert it back to formula use only when the user provides new input.)
Justin Klaassen44595162015-05-28 17:55:20 -0700903 // Historical note: In the Lollipop version, this invisibly and instantaneously moved
Hans Boehm84614952014-11-25 18:46:17 -0800904 // formula and result displays back at the end of the animation. We no longer do that,
905 // so that we can continue to properly support scrolling of the result.
906 // We assume the result already contains the text to be expanded.
907 private void onResult(boolean animate) {
Justin Klaassen44595162015-05-28 17:55:20 -0700908 // Calculate the textSize that would be used to display the result in the formula.
909 // For scrollable results just use the minimum textSize to maximize the number of digits
910 // that are visible on screen.
911 float textSize = mFormulaText.getMinimumTextSize();
912 if (!mResultText.isScrollable()) {
913 textSize = mFormulaText.getVariableTextSize(mResultText.getText().toString());
914 }
915
916 // Scale the result to match the calculated textSize, minimizing the jump-cut transition
917 // when a result is reused in a subsequent expression.
918 final float resultScale = textSize / mResultText.getTextSize();
919
920 // Set the result's pivot to match its gravity.
921 mResultText.setPivotX(mResultText.getWidth() - mResultText.getPaddingRight());
922 mResultText.setPivotY(mResultText.getHeight() - mResultText.getPaddingBottom());
923
924 // Calculate the necessary translations so the result takes the place of the formula and
925 // the formula moves off the top of the screen.
Annie Chin28589dc2016-06-09 17:50:51 -0700926 final float resultTranslationY = (mFormulaContainer.getBottom() - mResultText.getBottom())
927 - (mFormulaText.getPaddingBottom() - mResultText.getPaddingBottom());
928 float formulaTranslationY = -mFormulaContainer.getBottom();
Annie Chin26e159e2016-05-18 15:17:14 -0700929 if (mOneLine) {
930 // Position the result text.
931 mResultText.setY(mResultText.getBottom());
Annie Chin28589dc2016-06-09 17:50:51 -0700932 formulaTranslationY = -(findViewById(R.id.toolbar).getBottom()
933 + mFormulaContainer.getBottom());
Annie Chin26e159e2016-05-18 15:17:14 -0700934 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700935
Justin Klaassen44595162015-05-28 17:55:20 -0700936 // Change the result's textColor to match the formula.
937 final int formulaTextColor = mFormulaText.getCurrentTextColor();
938
Hans Boehm84614952014-11-25 18:46:17 -0800939 if (animate) {
Hans Boehmccc55662015-07-07 14:16:59 -0700940 mResultText.announceForAccessibility(getResources().getString(R.string.desc_eq));
941 mResultText.announceForAccessibility(mResultText.getText());
Hans Boehmc1ea0912015-06-19 15:05:07 -0700942 setState(CalculatorState.ANIMATE);
Hans Boehm84614952014-11-25 18:46:17 -0800943 final AnimatorSet animatorSet = new AnimatorSet();
944 animatorSet.playTogether(
Justin Klaassen44595162015-05-28 17:55:20 -0700945 ObjectAnimator.ofPropertyValuesHolder(mResultText,
946 PropertyValuesHolder.ofFloat(View.SCALE_X, resultScale),
947 PropertyValuesHolder.ofFloat(View.SCALE_Y, resultScale),
948 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, resultTranslationY)),
949 ObjectAnimator.ofArgb(mResultText, TEXT_COLOR, formulaTextColor),
Annie Chine918fd22016-03-09 11:07:54 -0800950 ObjectAnimator.ofFloat(mFormulaContainer, View.TRANSLATION_Y,
951 formulaTranslationY));
Justin Klaassen44595162015-05-28 17:55:20 -0700952 animatorSet.setDuration(getResources().getInteger(
953 android.R.integer.config_longAnimTime));
Hans Boehm84614952014-11-25 18:46:17 -0800954 animatorSet.addListener(new AnimatorListenerAdapter() {
955 @Override
Hans Boehm84614952014-11-25 18:46:17 -0800956 public void onAnimationEnd(Animator animation) {
957 setState(CalculatorState.RESULT);
958 mCurrentAnimator = null;
959 }
960 });
Justin Klaassen4b3af052014-05-27 17:53:10 -0700961
Hans Boehm84614952014-11-25 18:46:17 -0800962 mCurrentAnimator = animatorSet;
963 animatorSet.start();
964 } else /* No animation desired; get there fast, e.g. when restarting */ {
Justin Klaassen44595162015-05-28 17:55:20 -0700965 mResultText.setScaleX(resultScale);
966 mResultText.setScaleY(resultScale);
967 mResultText.setTranslationY(resultTranslationY);
968 mResultText.setTextColor(formulaTextColor);
Annie Chine918fd22016-03-09 11:07:54 -0800969 mFormulaContainer.setTranslationY(formulaTranslationY);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700970 setState(CalculatorState.RESULT);
Hans Boehm84614952014-11-25 18:46:17 -0800971 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700972 }
Hans Boehm84614952014-11-25 18:46:17 -0800973
974 // Restore positions of the formula and result displays back to their original,
975 // pre-animation state.
976 private void restoreDisplayPositions() {
977 // Clear result.
Justin Klaassen44595162015-05-28 17:55:20 -0700978 mResultText.setText("");
Hans Boehm84614952014-11-25 18:46:17 -0800979 // Reset all of the values modified during the animation.
Justin Klaassen44595162015-05-28 17:55:20 -0700980 mResultText.setScaleX(1.0f);
981 mResultText.setScaleY(1.0f);
982 mResultText.setTranslationX(0.0f);
983 mResultText.setTranslationY(0.0f);
Annie Chine918fd22016-03-09 11:07:54 -0800984 mFormulaContainer.setTranslationY(0.0f);
Hans Boehm84614952014-11-25 18:46:17 -0800985
Hans Boehm08e8f322015-04-21 13:18:38 -0700986 mFormulaText.requestFocus();
Hans Boehm5e6a0ca2015-09-22 17:09:01 -0700987 }
988
989 @Override
990 public void onClick(AlertDialogFragment fragment, int which) {
991 if (which == DialogInterface.BUTTON_POSITIVE) {
992 // Timeout extension request.
993 mEvaluator.setLongTimeOut();
994 }
995 }
Hans Boehm84614952014-11-25 18:46:17 -0800996
Justin Klaassend48b7562015-04-16 16:51:38 -0700997 @Override
998 public boolean onCreateOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -0700999 super.onCreateOptionsMenu(menu);
1000
1001 getMenuInflater().inflate(R.menu.activity_calculator, menu);
Justin Klaassend48b7562015-04-16 16:51:38 -07001002 return true;
1003 }
1004
1005 @Override
1006 public boolean onPrepareOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -07001007 super.onPrepareOptionsMenu(menu);
1008
1009 // Show the leading option when displaying a result.
1010 menu.findItem(R.id.menu_leading).setVisible(mCurrentState == CalculatorState.RESULT);
1011
1012 // Show the fraction option when displaying a rational result.
1013 menu.findItem(R.id.menu_fraction).setVisible(mCurrentState == CalculatorState.RESULT
Hans Boehm995e5eb2016-02-08 11:03:01 -08001014 && mEvaluator.getResult().exactlyDisplayable());
Justin Klaassend36d63e2015-05-05 12:59:36 -07001015
Justin Klaassend48b7562015-04-16 16:51:38 -07001016 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001017 }
1018
1019 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -07001020 public boolean onOptionsItemSelected(MenuItem item) {
Hans Boehm84614952014-11-25 18:46:17 -08001021 switch (item.getItemId()) {
Justin Klaassend36d63e2015-05-05 12:59:36 -07001022 case R.id.menu_leading:
1023 displayFull();
Hans Boehm84614952014-11-25 18:46:17 -08001024 return true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001025 case R.id.menu_fraction:
1026 displayFraction();
1027 return true;
Justin Klaassend36d63e2015-05-05 12:59:36 -07001028 case R.id.menu_licenses:
1029 startActivity(new Intent(this, Licenses.class));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001030 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001031 default:
1032 return super.onOptionsItemSelected(item);
1033 }
1034 }
1035
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001036 private void displayMessage(String s) {
Hans Boehm5e6a0ca2015-09-22 17:09:01 -07001037 AlertDialogFragment.showMessageDialog(this, s, null);
Hans Boehm84614952014-11-25 18:46:17 -08001038 }
1039
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001040 private void displayFraction() {
Hans Boehm995e5eb2016-02-08 11:03:01 -08001041 UnifiedReal result = mEvaluator.getResult();
Hans Boehm013969e2015-04-13 20:29:47 -07001042 displayMessage(KeyMaps.translateResult(result.toNiceString()));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001043 }
1044
1045 // Display full result to currently evaluated precision
1046 private void displayFull() {
1047 Resources res = getResources();
Justin Klaassen44595162015-05-28 17:55:20 -07001048 String msg = mResultText.getFullText() + " ";
1049 if (mResultText.fullTextIsExact()) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001050 msg += res.getString(R.string.exact);
1051 } else {
1052 msg += res.getString(R.string.approximate);
1053 }
1054 displayMessage(msg);
1055 }
1056
Hans Boehm017de982015-06-10 17:46:03 -07001057 /**
1058 * Add input characters to the end of the expression.
1059 * Map them to the appropriate button pushes when possible. Leftover characters
1060 * are added to mUnprocessedChars, which is presumed to immediately precede the newly
1061 * added characters.
Hans Boehm65a99a42016-02-03 18:16:07 -08001062 * @param moreChars characters to be added
1063 * @param explicit these characters were explicitly typed by the user, not pasted
Hans Boehm017de982015-06-10 17:46:03 -07001064 */
1065 private void addChars(String moreChars, boolean explicit) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001066 if (mUnprocessedChars != null) {
1067 moreChars = mUnprocessedChars + moreChars;
1068 }
1069 int current = 0;
1070 int len = moreChars.length();
Hans Boehm0b9806f2015-06-29 16:07:15 -07001071 boolean lastWasDigit = false;
Hans Boehm5d79d102015-09-16 16:33:47 -07001072 if (mCurrentState == CalculatorState.RESULT && len != 0) {
1073 // Clear display immediately for incomplete function name.
1074 switchToInput(KeyMaps.keyForChar(moreChars.charAt(current)));
1075 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001076 while (current < len) {
1077 char c = moreChars.charAt(current);
Hans Boehm013969e2015-04-13 20:29:47 -07001078 int k = KeyMaps.keyForChar(c);
Hans Boehm0b9806f2015-06-29 16:07:15 -07001079 if (!explicit) {
1080 int expEnd;
1081 if (lastWasDigit && current !=
1082 (expEnd = Evaluator.exponentEnd(moreChars, current))) {
1083 // Process scientific notation with 'E' when pasting, in spite of ambiguity
1084 // with base of natural log.
1085 // Otherwise the 10^x key is the user's friend.
1086 mEvaluator.addExponent(moreChars, current, expEnd);
1087 current = expEnd;
1088 lastWasDigit = false;
1089 continue;
1090 } else {
1091 boolean isDigit = KeyMaps.digVal(k) != KeyMaps.NOT_DIGIT;
1092 if (current == 0 && (isDigit || k == R.id.dec_point)
1093 && mEvaluator.getExpr().hasTrailingConstant()) {
1094 // Refuse to concatenate pasted content to trailing constant.
1095 // This makes pasting of calculator results more consistent, whether or
1096 // not the old calculator instance is still around.
1097 addKeyToExpr(R.id.op_mul);
1098 }
1099 lastWasDigit = (isDigit || lastWasDigit && k == R.id.dec_point);
1100 }
1101 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001102 if (k != View.NO_ID) {
1103 mCurrentButton = findViewById(k);
Hans Boehm017de982015-06-10 17:46:03 -07001104 if (explicit) {
1105 addExplicitKeyToExpr(k);
1106 } else {
1107 addKeyToExpr(k);
1108 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001109 if (Character.isSurrogate(c)) {
1110 current += 2;
1111 } else {
1112 ++current;
1113 }
1114 continue;
1115 }
Hans Boehm013969e2015-04-13 20:29:47 -07001116 int f = KeyMaps.funForString(moreChars, current);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001117 if (f != View.NO_ID) {
1118 mCurrentButton = findViewById(f);
Hans Boehm017de982015-06-10 17:46:03 -07001119 if (explicit) {
1120 addExplicitKeyToExpr(f);
1121 } else {
1122 addKeyToExpr(f);
1123 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001124 if (f == R.id.op_sqrt) {
1125 // Square root entered as function; don't lose the parenthesis.
1126 addKeyToExpr(R.id.lparen);
1127 }
1128 current = moreChars.indexOf('(', current) + 1;
1129 continue;
1130 }
1131 // There are characters left, but we can't convert them to button presses.
1132 mUnprocessedChars = moreChars.substring(current);
1133 redisplayAfterFormulaChange();
Hans Boehm52d477a2016-04-01 17:42:50 -07001134 showOrHideToolbar();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001135 return;
1136 }
1137 mUnprocessedChars = null;
1138 redisplayAfterFormulaChange();
Hans Boehm52d477a2016-04-01 17:42:50 -07001139 showOrHideToolbar();
Hans Boehm84614952014-11-25 18:46:17 -08001140 }
1141
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001142 @Override
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001143 public boolean onPaste(ClipData clip) {
1144 final ClipData.Item item = clip.getItemCount() == 0 ? null : clip.getItemAt(0);
1145 if (item == null) {
1146 // nothing to paste, bail early...
1147 return false;
1148 }
1149
1150 // Check if the item is a previously copied result, otherwise paste as raw text.
1151 final Uri uri = item.getUri();
1152 if (uri != null && mEvaluator.isLastSaved(uri)) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001153 if (mCurrentState == CalculatorState.ERROR
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001154 || mCurrentState == CalculatorState.RESULT) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001155 setState(CalculatorState.INPUT);
1156 mEvaluator.clear();
Hans Boehm84614952014-11-25 18:46:17 -08001157 }
Hans Boehm3666e632015-07-27 18:33:12 -07001158 mEvaluator.appendSaved();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001159 redisplayAfterFormulaChange();
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001160 } else {
Hans Boehm017de982015-06-10 17:46:03 -07001161 addChars(item.coerceToText(this).toString(), false);
Hans Boehm84614952014-11-25 18:46:17 -08001162 }
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001163 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001164 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001165}