blob: eee9fa87045bc659a9ac2a0a34aeb674f64fb369 [file] [log] [blame]
Hans Boehm84614952014-11-25 18:46:17 -08001/*
Hans Boehm995e5eb2016-02-08 11:03:01 -08002 * Copyright (C) 2016 The Android Open Source Project
Hans Boehm84614952014-11-25 18:46:17 -08003 *
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 Boehm84614952014-11-25 18:46:17 -080017package com.android.calculator2;
18
Hans Boehm83f278e2016-12-19 16:20:03 -080019import android.content.Context;
Justin Klaassen8b1efdb2015-06-22 15:10:53 -070020import android.content.SharedPreferences;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070021import android.net.Uri;
22import android.os.AsyncTask;
Hans Boehm84614952014-11-25 18:46:17 -080023import android.os.Handler;
Justin Klaassen8b1efdb2015-06-22 15:10:53 -070024import android.preference.PreferenceManager;
Aurimas Liutikas8c43f062018-03-28 08:10:28 -070025import androidx.annotation.NonNull;
26import androidx.annotation.StringRes;
27import androidx.annotation.VisibleForTesting;
Annie Chin06fd3cf2016-11-07 16:04:33 -080028import android.text.Spannable;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070029import android.util.Log;
Hans Boehm84614952014-11-25 18:46:17 -080030
Annie Chin56bcbf12016-09-23 17:04:22 -070031import com.hp.creals.CR;
Hans Boehm84614952014-11-25 18:46:17 -080032
Hans Boehm8f051c32016-10-03 16:53:58 -070033import java.io.ByteArrayInputStream;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070034import java.io.DataInput;
Hans Boehm8f051c32016-10-03 16:53:58 -070035import java.io.DataInputStream;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070036import java.io.DataOutput;
37import java.io.IOException;
Justin Klaassene2711cb2015-05-28 11:13:17 -070038import java.text.DateFormat;
39import java.text.SimpleDateFormat;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070040import java.util.Date;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070041import java.util.Random;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070042import java.util.TimeZone;
Annie Chin06fd3cf2016-11-07 16:04:33 -080043import java.util.concurrent.ConcurrentHashMap;
44import java.util.concurrent.atomic.AtomicReference;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070045
Hans Boehm3666e632015-07-27 18:33:12 -070046/**
Hans Boehm8f051c32016-10-03 16:53:58 -070047 * This implements the calculator evaluation logic.
48 * Logically this maintains a signed integer indexed set of expressions, one of which
49 * is distinguished as the main expression.
50 * The main expression is constructed and edited with append(), delete(), etc.
51 * An evaluation an then be started with a call to evaluateAndNotify() or requireResult().
52 * This starts an asynchronous computation, which requests display of the initial result, when
53 * available. When initial evaluation is complete, it calls the associated listener's
54 * onEvaluate() method. This occurs in a separate event, possibly quite a bit later. Once a
55 * result has been computed, and before the underlying expression is modified, the
56 * getString(index) method may be used to produce Strings that represent approximations to various
Hans Boehm3666e632015-07-27 18:33:12 -070057 * precisions.
58 *
59 * Actual expressions being evaluated are represented as {@link CalculatorExpr}s.
60 *
Hans Boehm8f051c32016-10-03 16:53:58 -070061 * The Evaluator holds the expressions and all associated state needed for evaluating
62 * them. It provides functionality for saving and restoring this state. However the underlying
63 * CalculatorExprs are exposed to the client, and may be directly accessed after cancelling any
Hans Boehm3666e632015-07-27 18:33:12 -070064 * in-progress computations by invoking the cancelAll() method.
65 *
66 * When evaluation is requested, we invoke the eval() method on the CalculatorExpr from a
Hans Boehm8f051c32016-10-03 16:53:58 -070067 * background AsyncTask. A subsequent getString() call for the same expression index returns
68 * immediately, though it may return a result containing placeholder ' ' characters. If we had to
69 * return palceholder characters, we start a background task, which invokes the onReevaluate()
70 * callback when it completes. In either case, the background task computes the appropriate
71 * result digits by evaluating the UnifiedReal returned by CalculatorExpr.eval() to the required
Hans Boehm3666e632015-07-27 18:33:12 -070072 * precision.
73 *
74 * We cache the best decimal approximation we have already computed. We compute generously to
75 * allow for some scrolling without recomputation and to minimize the chance of digits flipping
76 * from "0000" to "9999". The best known result approximation is maintained as a string by
Hans Boehm995e5eb2016-02-08 11:03:01 -080077 * mResultString (and often in a different format by the CR representation of the result). When
78 * we are in danger of not having digits to display in response to further scrolling, we also
79 * initiate a background computation to higher precision, as if we had generated placeholder
80 * characters.
Hans Boehm3666e632015-07-27 18:33:12 -070081 *
82 * The code is designed to ensure that the error in the displayed result (excluding any
83 * placeholder characters) is always strictly less than 1 in the last displayed digit. Typically
84 * we actually display a prefix of a result that has this property and additionally is computed to
85 * a significantly higher precision. Thus we almost always round correctly towards zero. (Fully
86 * correct rounding towards zero is not computable, at least given our representation.)
87 *
88 * Initial expression evaluation may time out. This may happen in the case of domain errors such
89 * as division by zero, or for large computations. We do not currently time out reevaluations to
90 * higher precision, since the original evaluation precluded a domain error that could result in
91 * non-termination. (We may discover that a presumed zero result is actually slightly negative
92 * when re-evaluated; but that results in an exception, which we can handle.) The user can abort
93 * either kind of computation.
94 *
95 * We ensure that only one evaluation of either kind (AsyncEvaluator or AsyncReevaluator) is
96 * running at a time.
97 */
Hans Boehm8f051c32016-10-03 16:53:58 -070098public class Evaluator implements CalculatorExpr.ExprResolver {
99
Annie Chin06fd3cf2016-11-07 16:04:33 -0800100 private static Evaluator evaluator;
101
Annie Chin532b77e2016-12-06 13:30:35 -0800102 public static String TIMEOUT_DIALOG_TAG = "timeout";
103
Annie Chine7c32322017-01-30 18:08:56 -0800104 @NonNull
Christine Frankscbc51fa2017-01-04 21:00:36 -0800105 public static Evaluator getInstance(Context context) {
Annie Chin06fd3cf2016-11-07 16:04:33 -0800106 if (evaluator == null) {
Christine Frankscbc51fa2017-01-04 21:00:36 -0800107 evaluator = new Evaluator(context.getApplicationContext());
Annie Chin06fd3cf2016-11-07 16:04:33 -0800108 }
109 return evaluator;
110 }
111
Hans Boehm8f051c32016-10-03 16:53:58 -0700112 public interface EvaluationListener {
113 /**
114 * Called if evaluation was explicitly cancelled or evaluation timed out.
115 */
116 public void onCancelled(long index);
117 /**
118 * Called if evaluation resulted in an error.
119 */
120 public void onError(long index, int errorId);
121 /**
122 * Called if evaluation completed normally.
123 * @param index index of expression whose evaluation completed
124 * @param initPrecOffset the offset used for initial evaluation
125 * @param msdIndex index of first non-zero digit in the computed result string
126 * @param lsdOffset offset of last digit in result if result has finite decimal
127 * expansion
128 * @param truncatedWholePart the integer part of the result
129 */
130 public void onEvaluate(long index, int initPrecOffset, int msdIndex, int lsdOffset,
131 String truncatedWholePart);
132 /**
133 * Called in response to a reevaluation request, once more precision is available.
134 * Typically the listener wil respond by calling getString() to retrieve the new
135 * better approximation.
136 */
137 public void onReevaluate(long index); // More precision is now available; please redraw.
138 }
139
140 /**
141 * A query interface for derived information based on character widths.
142 * This provides information we need to calculate the "preferred precision offset" used
143 * to display the initial result. It's used to compute the number of digits we can actually
144 * display. All methods are callable from any thread.
145 */
146 public interface CharMetricsInfo {
147 /**
148 * Return the maximum number of (adjusted, digit-width) characters that will fit in the
149 * result display. May be called asynchronously from non-UI thread.
150 */
151 public int getMaxChars();
152 /**
153 * Return the number of additional digit widths required to add digit separators to
154 * the supplied string prefix.
155 * The prefix consists of the first len characters of string s, which is presumed to
156 * represent a whole number. Callable from non-UI thread.
Hans Boehmd4959e82016-11-15 18:01:28 -0800157 * Returns zero if metrics information is not yet available.
Hans Boehm8f051c32016-10-03 16:53:58 -0700158 */
159 public float separatorChars(String s, int len);
160 /**
161 * Return extra width credit for presence of a decimal point, as fraction of a digit width.
162 * May be called by non-UI thread.
163 */
164 public float getDecimalCredit();
165 /**
166 * Return extra width credit for absence of ellipsis, as fraction of a digit width.
167 * May be called by non-UI thread.
168 */
169 public float getNoEllipsisCredit();
170 }
171
172 /**
173 * A CharMetricsInfo that can be used when we are really only interested in computing
174 * short representations to be embedded on formulas.
175 */
176 private class DummyCharMetricsInfo implements CharMetricsInfo {
177 @Override
178 public int getMaxChars() {
179 return SHORT_TARGET_LENGTH + 10;
180 }
181 @Override
182 public float separatorChars(String s, int len) {
183 return 0;
184 }
185 @Override
186 public float getDecimalCredit() {
187 return 0;
188 }
189 @Override
190 public float getNoEllipsisCredit() {
191 return 0;
192 }
193 }
194
195 private final DummyCharMetricsInfo mDummyCharMetricsInfo = new DummyCharMetricsInfo();
196
197 public static final long MAIN_INDEX = 0; // Index of main expression.
198 // Once final evaluation of an expression is complete, or when we need to save
Hans Boehm31ea2522016-11-23 17:47:02 -0800199 // a partial result, we copy the main expression to a non-zero index.
Hans Boehm8f051c32016-10-03 16:53:58 -0700200 // At that point, the expression no longer changes, and is preserved
201 // until the entire history is cleared. Only expressions at nonzero indices
202 // may be embedded in other expressions.
Hans Boehm31ea2522016-11-23 17:47:02 -0800203 // Each expression index can only have one outstanding evaluation request at a time.
204 // To avoid conflicts between the history and main View, we copy the main expression
205 // to allow independent evaluation by both.
206 public static final long HISTORY_MAIN_INDEX = -1; // Read-only copy of main expression.
Hans Boehm8f051c32016-10-03 16:53:58 -0700207 // To update e.g. "memory" contents, we copy the corresponding expression to a permanent
208 // index, and then remember that index.
209 private long mSavedIndex; // Index of "saved" expression mirroring clipboard. 0 if unused.
210 private long mMemoryIndex; // Index of "memory" expression. 0 if unused.
Justin Klaassenecc69fd2015-05-07 10:30:57 -0700211
Hans Boehm3666e632015-07-27 18:33:12 -0700212 // When naming variables and fields, "Offset" denotes a character offset in a string
213 // representing a decimal number, where the offset is relative to the decimal point. 1 =
214 // tenths position, -1 = units position. Integer.MAX_VALUE is sometimes used for the offset
215 // of the last digit in an a nonterminating decimal expansion. We use the suffix "Index" to
Hans Boehm8f051c32016-10-03 16:53:58 -0700216 // denote a zero-based absolute index into such a string. (In other contexts, like above,
217 // we also use "index" to refer to the key in mExprs below, the list of all known
218 // expressions.)
Hans Boehm3666e632015-07-27 18:33:12 -0700219
Justin Klaassen8b1efdb2015-06-22 15:10:53 -0700220 private static final String KEY_PREF_DEGREE_MODE = "degree_mode";
Hans Boehm8f051c32016-10-03 16:53:58 -0700221 private static final String KEY_PREF_SAVED_INDEX = "saved_index";
222 private static final String KEY_PREF_MEMORY_INDEX = "memory_index";
223 private static final String KEY_PREF_SAVED_NAME = "saved_name";
Justin Klaassen8b1efdb2015-06-22 15:10:53 -0700224
Hans Boehm3666e632015-07-27 18:33:12 -0700225 // The minimum number of extra digits we always try to compute to improve the chance of
226 // producing a correctly-rounded-towards-zero result. The extra digits can be displayed to
227 // avoid generating placeholder digits, but should only be displayed briefly while computing.
Hans Boehm08e8f322015-04-21 13:18:38 -0700228 private static final int EXTRA_DIGITS = 20;
Hans Boehm84614952014-11-25 18:46:17 -0800229
Hans Boehm3666e632015-07-27 18:33:12 -0700230 // We adjust EXTRA_DIGITS by adding the length of the previous result divided by
231 // EXTRA_DIVISOR. This helps hide recompute latency when long results are requested;
232 // We start the recomputation substantially before the need is likely to be visible.
233 private static final int EXTRA_DIVISOR = 5;
234
235 // In addition to insisting on extra digits (see above), we minimize reevaluation
236 // frequency by precomputing an extra PRECOMPUTE_DIGITS
237 // + <current_precision_offset>/PRECOMPUTE_DIVISOR digits, whenever we are forced to
238 // reevaluate. The last term is dropped if prec < 0.
239 private static final int PRECOMPUTE_DIGITS = 30;
240 private static final int PRECOMPUTE_DIVISOR = 5;
241
242 // Initial evaluation precision. Enough to guarantee that we can compute the short
243 // representation, and that we rarely have to evaluate nonzero results to MAX_MSD_PREC_OFFSET.
244 // It also helps if this is at least EXTRA_DIGITS + display width, so that we don't
245 // immediately need a second evaluation.
Hans Boehm50ed3202015-06-09 14:35:49 -0700246 private static final int INIT_PREC = 50;
Hans Boehm3666e632015-07-27 18:33:12 -0700247
248 // The largest number of digits to the right of the decimal point to which we will evaluate to
249 // compute proper scientific notation for values close to zero. Chosen to ensure that we
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800250 // always to better than IEEE double precision at identifying nonzeros. And then some.
251 // This is used only when we cannot a priori determine the most significant digit position, as
Hans Boehm995e5eb2016-02-08 11:03:01 -0800252 // we always can if we have a rational representation.
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800253 private static final int MAX_MSD_PREC_OFFSET = 1100;
Hans Boehm3666e632015-07-27 18:33:12 -0700254
255 // If we can replace an exponent by this many leading zeroes, we do so. Also used in
256 // estimating exponent size for truncating short representation.
Hans Boehm50ed3202015-06-09 14:35:49 -0700257 private static final int EXP_COST = 3;
Hans Boehm84614952014-11-25 18:46:17 -0800258
Christine Frankscbc51fa2017-01-04 21:00:36 -0800259 // Listener that reports changes to the state (empty/filled) of memory. Protected for testing.
260 private Callback mCallback;
261
262 // Context for database helper.
263 private Context mContext;
Hans Boehm3666e632015-07-27 18:33:12 -0700264
265 // A hopefully unique name associated with mSaved.
266 private String mSavedName;
267
Hans Boehm8f051c32016-10-03 16:53:58 -0700268 // The main expression may have changed since the last evaluation in ways that would affect its
Hans Boehm3666e632015-07-27 18:33:12 -0700269 // value.
Hans Boehmc023b732015-04-29 11:30:47 -0700270 private boolean mChangedValue;
Hans Boehmc023b732015-04-29 11:30:47 -0700271
Hans Boehm52d477a2016-04-01 17:42:50 -0700272 // The main expression contains trig functions.
273 private boolean mHasTrigFuncs;
274
Hans Boehm8f051c32016-10-03 16:53:58 -0700275 public static final int INVALID_MSD = Integer.MAX_VALUE;
Justin Klaassen8b1efdb2015-06-22 15:10:53 -0700276
Hans Boehm03b34512016-12-13 11:32:20 -0800277 // Used to represent an erroneous result or a required evaluation. Not displayed.
Christine Frankscbc51fa2017-01-04 21:00:36 -0800278 private static final String ERRONEOUS_RESULT = "ERR";
Hans Boehm849d8a42016-12-12 18:19:55 -0800279
Hans Boehm8f051c32016-10-03 16:53:58 -0700280 /**
281 * An individual CalculatorExpr, together with its evaluation state.
Hans Boehm31ea2522016-11-23 17:47:02 -0800282 * Only the main expression may be changed in-place. The HISTORY_MAIN_INDEX expression is
283 * periodically reset to be a fresh immutable copy of the main expression.
Hans Boehm8f051c32016-10-03 16:53:58 -0700284 * All other expressions are only added and never removed. The expressions themselves are
285 * never modified.
286 * All fields other than mExpr and mVal are touched only by the UI thread.
287 * For MAIN_INDEX, mExpr and mVal may change, but are also only ever touched by the UI thread.
288 * For all other expressions, mExpr does not change once the ExprInfo has been (atomically)
289 * added to mExprs. mVal may be asynchronously set by any thread, but we take care that it
290 * does not change after that. mDegreeMode is handled exactly like mExpr.
291 */
292 private class ExprInfo {
293 public CalculatorExpr mExpr; // The expression itself.
294 public boolean mDegreeMode; // Evaluating in degree, not radian, mode.
295 public ExprInfo(CalculatorExpr expr, boolean dm) {
296 mExpr = expr;
297 mDegreeMode = dm;
298 mVal = new AtomicReference<UnifiedReal>();
299 }
300
Hans Boehmd4959e82016-11-15 18:01:28 -0800301 // Currently running expression evaluator, if any. This is either an AsyncEvaluator
302 // (if mResultString == null or it's obsolete), or an AsyncReevaluator.
Hans Boehm31ea2522016-11-23 17:47:02 -0800303 // We arrange that only one evaluator is active at a time, in part by maintaining
304 // two separate ExprInfo structure for the main and history view, so that they can
305 // arrange for independent evaluators.
Hans Boehm8f051c32016-10-03 16:53:58 -0700306 public AsyncTask mEvaluator;
307
308 // The remaining fields are valid only if an evaluation completed successfully.
309 // mVal always points to an AtomicReference, but that may be null.
310 public AtomicReference<UnifiedReal> mVal;
311 // We cache the best known decimal result in mResultString. Whenever that is
312 // non-null, it is computed to exactly mResultStringOffset, which is always > 0.
313 // Valid only if mResultString is non-null and (for the main expression) !mChangedValue.
Hans Boehm849d8a42016-12-12 18:19:55 -0800314 // ERRONEOUS_RESULT indicates evaluation resulted in an error.
Hans Boehm8f051c32016-10-03 16:53:58 -0700315 public String mResultString;
316 public int mResultStringOffset = 0;
317 // Number of digits to which (possibly incomplete) evaluation has been requested.
318 // Only accessed by UI thread.
319 public int mResultStringOffsetReq = 0;
320 // Position of most significant digit in current cached result, if determined. This is just
321 // the index in mResultString holding the msd.
322 public int mMsdIndex = INVALID_MSD;
323 // Long timeout needed for evaluation?
324 public boolean mLongTimeout = false;
325 public long mTimeStamp;
Hans Boehm8f051c32016-10-03 16:53:58 -0700326 }
327
328 private ConcurrentHashMap<Long, ExprInfo> mExprs = new ConcurrentHashMap<Long, ExprInfo>();
329
330 // The database holding persistent expressions.
331 private ExpressionDB mExprDB;
332
333 private ExprInfo mMainExpr; // == mExprs.get(MAIN_INDEX)
334
335 private SharedPreferences mSharedPrefs;
Hans Boehm3666e632015-07-27 18:33:12 -0700336
337 private final Handler mTimeoutHandler; // Used to schedule evaluation timeouts.
338
Hans Boehm8f051c32016-10-03 16:53:58 -0700339 private void setMainExpr(ExprInfo expr) {
340 mMainExpr = expr;
341 mExprs.put(MAIN_INDEX, expr);
342 }
Hans Boehm3666e632015-07-27 18:33:12 -0700343
Hans Boehm83f278e2016-12-19 16:20:03 -0800344 Evaluator(Context context) {
Christine Frankscbc51fa2017-01-04 21:00:36 -0800345 mContext = context;
Hans Boehm8f051c32016-10-03 16:53:58 -0700346 setMainExpr(new ExprInfo(new CalculatorExpr(), false));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700347 mSavedName = "none";
Hans Boehm84614952014-11-25 18:46:17 -0800348 mTimeoutHandler = new Handler();
Justin Klaassen8b1efdb2015-06-22 15:10:53 -0700349
Hans Boehm83f278e2016-12-19 16:20:03 -0800350 mExprDB = new ExpressionDB(context);
351 mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
Hans Boehm8f051c32016-10-03 16:53:58 -0700352 mMainExpr.mDegreeMode = mSharedPrefs.getBoolean(KEY_PREF_DEGREE_MODE, false);
353 long savedIndex = mSharedPrefs.getLong(KEY_PREF_SAVED_INDEX, 0L);
354 long memoryIndex = mSharedPrefs.getLong(KEY_PREF_MEMORY_INDEX, 0L);
Hans Boehm8bf0dca2017-01-25 17:10:39 -0800355 if (savedIndex != 0 && savedIndex != -1 /* Recover from old corruption */) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700356 setSavedIndexWhenEvaluated(savedIndex);
357 }
Hans Boehm8bf0dca2017-01-25 17:10:39 -0800358 if (memoryIndex != 0 && memoryIndex != -1) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700359 setMemoryIndexWhenEvaluated(memoryIndex, false /* no need to persist again */);
360 }
361 mSavedName = mSharedPrefs.getString(KEY_PREF_SAVED_NAME, "none");
362 }
363
364 /**
365 * Retrieve minimum expression index.
366 * This is the minimum over all expressions, including uncached ones residing only
367 * in the data base. If no expressions with negative indices were preserved, this will
368 * return a small negative predefined constant.
369 * May be called from any thread, but will block until the database is opened.
370 */
371 public long getMinIndex() {
372 return mExprDB.getMinIndex();
373 }
374
375 /**
376 * Retrieve maximum expression index.
377 * This is the maximum over all expressions, including uncached ones residing only
378 * in the data base. If no expressions with positive indices were preserved, this will
379 * return 0.
380 * May be called from any thread, but will block until the database is opened.
381 */
382 public long getMaxIndex() {
383 return mExprDB.getMaxIndex();
Hans Boehm84614952014-11-25 18:46:17 -0800384 }
385
Hans Boehm3666e632015-07-27 18:33:12 -0700386 /**
Christine Frankscbc51fa2017-01-04 21:00:36 -0800387 * Set the Callback for showing dialogs and notifying the UI about memory state changes.
388 * @param callback
389 */
390 public void setCallback(Callback callback) {
391 mCallback = callback;
392 }
393
394 /**
Hans Boehm8bf0dca2017-01-25 17:10:39 -0800395 * Does the expression index refer to a transient and mutable expression?
396 */
397 private boolean isMutableIndex(long index) {
398 return index == MAIN_INDEX || index == HISTORY_MAIN_INDEX;
399 }
400
401 /**
Hans Boehm3666e632015-07-27 18:33:12 -0700402 * Result of initial asynchronous result computation.
403 * Represents either an error or a result computed to an initial evaluation precision.
404 */
Hans Boehm84614952014-11-25 18:46:17 -0800405 private static class InitialResult {
Hans Boehm3666e632015-07-27 18:33:12 -0700406 public final int errorResourceId; // Error string or INVALID_RES_ID.
Hans Boehm995e5eb2016-02-08 11:03:01 -0800407 public final UnifiedReal val; // Constructive real value.
Hans Boehm3666e632015-07-27 18:33:12 -0700408 public final String newResultString; // Null iff it can't be computed.
409 public final int newResultStringOffset;
410 public final int initDisplayOffset;
Hans Boehm995e5eb2016-02-08 11:03:01 -0800411 InitialResult(UnifiedReal v, String s, int p, int idp) {
Hans Boehm3666e632015-07-27 18:33:12 -0700412 errorResourceId = Calculator.INVALID_RES_ID;
413 val = v;
Hans Boehm3666e632015-07-27 18:33:12 -0700414 newResultString = s;
415 newResultStringOffset = p;
416 initDisplayOffset = idp;
Hans Boehm84614952014-11-25 18:46:17 -0800417 }
Hans Boehm3666e632015-07-27 18:33:12 -0700418 InitialResult(int errorId) {
419 errorResourceId = errorId;
Hans Boehm995e5eb2016-02-08 11:03:01 -0800420 val = UnifiedReal.ZERO;
Hans Boehm3666e632015-07-27 18:33:12 -0700421 newResultString = "BAD";
422 newResultStringOffset = 0;
423 initDisplayOffset = 0;
Hans Boehm84614952014-11-25 18:46:17 -0800424 }
425 boolean isError() {
Hans Boehm3666e632015-07-27 18:33:12 -0700426 return errorResourceId != Calculator.INVALID_RES_ID;
Hans Boehm84614952014-11-25 18:46:17 -0800427 }
Hans Boehm84614952014-11-25 18:46:17 -0800428 }
429
430 private void displayCancelledMessage() {
Christine Frankscbc51fa2017-01-04 21:00:36 -0800431 if (mCallback != null) {
432 mCallback.showMessageDialog(0, R.string.cancelled, 0, null);
433 }
Hans Boehm84614952014-11-25 18:46:17 -0800434 }
435
Hans Boehm5e6a0ca2015-09-22 17:09:01 -0700436 // Timeout handling.
437 // Expressions are evaluated with a sort timeout or a long timeout.
438 // Each implies different maxima on both computation time and bit length.
439 // We recheck bit length separetly to avoid wasting time on decimal conversions that are
440 // destined to fail.
Hans Boehm3666e632015-07-27 18:33:12 -0700441
Hans Boehm5e6a0ca2015-09-22 17:09:01 -0700442 /**
Hans Boehm5e6a0ca2015-09-22 17:09:01 -0700443 * Return the timeout in milliseconds.
444 * @param longTimeout a long timeout is in effect
445 */
446 private long getTimeout(boolean longTimeout) {
447 return longTimeout ? 15000 : 2000;
448 // Exceeding a few tens of seconds increases the risk of running out of memory
449 // and impacting the rest of the system.
Hans Boehm84614952014-11-25 18:46:17 -0800450 }
451
Hans Boehm5e6a0ca2015-09-22 17:09:01 -0700452 /**
453 * Return the maximum number of bits in the result. Longer results are assumed to time out.
454 * @param longTimeout a long timeout is in effect
455 */
456 private int getMaxResultBits(boolean longTimeout) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700457 return longTimeout ? 700000 : 240000;
Hans Boehm5e6a0ca2015-09-22 17:09:01 -0700458 }
459
460 /**
461 * Timeout for unrequested, speculative evaluations, in milliseconds.
462 */
Hans Boehm8f051c32016-10-03 16:53:58 -0700463 private static final long QUICK_TIMEOUT = 1000;
Hans Boehm5e6a0ca2015-09-22 17:09:01 -0700464
465 /**
Hans Boehmcc368502016-12-09 10:44:46 -0800466 * Timeout for non-MAIN expressions. Note that there may be many such evaluations in
467 * progress on the same thread or core. Thus the evaluation latency may include that needed
468 * to complete previously enqueued evaluations. Thus the longTimeout flag is not very
469 * meaningful, and currently ignored.
470 * Since this is only used for expressions that we have previously successfully evaluated,
471 * these timeouts hsould never trigger.
472 */
473 private static final long NON_MAIN_TIMEOUT = 100000;
474
475 /**
Hans Boehm5e6a0ca2015-09-22 17:09:01 -0700476 * Maximum result bit length for unrequested, speculative evaluations.
Hans Boehm995e5eb2016-02-08 11:03:01 -0800477 * Also used to bound evaluation precision for small non-zero fractions.
Hans Boehm5e6a0ca2015-09-22 17:09:01 -0700478 */
Hans Boehmbd01e4b2016-11-23 10:12:58 -0800479 private static final int QUICK_MAX_RESULT_BITS = 150000;
Hans Boehm5e6a0ca2015-09-22 17:09:01 -0700480
Hans Boehm8f051c32016-10-03 16:53:58 -0700481 private void displayTimeoutMessage(boolean longTimeout) {
Christine Frankscbc51fa2017-01-04 21:00:36 -0800482 if (mCallback != null) {
483 mCallback.showMessageDialog(R.string.dialog_timeout, R.string.timeout,
484 longTimeout ? 0 : R.string.ok_remove_timeout, TIMEOUT_DIALOG_TAG);
485 }
Hans Boehm5e6a0ca2015-09-22 17:09:01 -0700486 }
487
Hans Boehm8f051c32016-10-03 16:53:58 -0700488 public void setLongTimeout() {
489 mMainExpr.mLongTimeout = true;
Hans Boehm5e6a0ca2015-09-22 17:09:01 -0700490 }
491
492 /**
493 * Compute initial cache contents and result when we're good and ready.
494 * We leave the expression display up, with scrolling disabled, until this computation
495 * completes. Can result in an error display if something goes wrong. By default we set a
496 * timeout to catch runaway computations.
497 */
Hans Boehm3666e632015-07-27 18:33:12 -0700498 class AsyncEvaluator extends AsyncTask<Void, Void, InitialResult> {
Hans Boehm84614952014-11-25 18:46:17 -0800499 private boolean mDm; // degrees
Hans Boehmd4959e82016-11-15 18:01:28 -0800500 public boolean mRequired; // Result was requested by user.
Hans Boehmc1ea0912015-06-19 15:05:07 -0700501 private boolean mQuiet; // Suppress cancellation message.
Hans Boehmc023b732015-04-29 11:30:47 -0700502 private Runnable mTimeoutRunnable = null;
Hans Boehm8f051c32016-10-03 16:53:58 -0700503 private EvaluationListener mListener; // Completion callback.
504 private CharMetricsInfo mCharMetricsInfo; // Where to get result size information.
505 private long mIndex; // Expression index.
506 private ExprInfo mExprInfo; // Current expression.
507
508 AsyncEvaluator(long index, EvaluationListener listener, CharMetricsInfo cmi, boolean dm,
509 boolean required) {
510 mIndex = index;
511 mListener = listener;
512 mCharMetricsInfo = cmi;
Hans Boehm84614952014-11-25 18:46:17 -0800513 mDm = dm;
514 mRequired = required;
Hans Boehm8f051c32016-10-03 16:53:58 -0700515 mQuiet = !required || mIndex != MAIN_INDEX;
516 mExprInfo = mExprs.get(mIndex);
517 if (mExprInfo.mEvaluator != null) {
518 throw new AssertionError("Evaluation already in progress!");
519 }
Hans Boehm84614952014-11-25 18:46:17 -0800520 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700521
522 private void handleTimeout() {
523 // Runs in UI thread.
Hans Boehmc023b732015-04-29 11:30:47 -0700524 boolean running = (getStatus() != AsyncTask.Status.FINISHED);
525 if (running && cancel(true)) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700526 mExprs.get(mIndex).mEvaluator = null;
527 if (mRequired && mIndex == MAIN_INDEX) {
528 // Replace mExpr with clone to avoid races if task still runs for a while.
529 mMainExpr.mExpr = (CalculatorExpr)mMainExpr.mExpr.clone();
Hans Boehmc1ea0912015-06-19 15:05:07 -0700530 suppressCancelMessage();
Hans Boehm8f051c32016-10-03 16:53:58 -0700531 displayTimeoutMessage(mExprInfo.mLongTimeout);
Hans Boehmc023b732015-04-29 11:30:47 -0700532 }
533 }
534 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700535
Hans Boehmc1ea0912015-06-19 15:05:07 -0700536 private void suppressCancelMessage() {
537 mQuiet = true;
538 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700539
Hans Boehm84614952014-11-25 18:46:17 -0800540 @Override
541 protected void onPreExecute() {
Hans Boehm8f051c32016-10-03 16:53:58 -0700542 long timeout = mRequired ? getTimeout(mExprInfo.mLongTimeout) : QUICK_TIMEOUT;
543 if (mIndex != MAIN_INDEX) {
544 // We evaluated the expression before with the current timeout, so this shouldn't
545 // ever time out. We evaluate it with a ridiculously long timeout to avoid running
546 // down the battery if something does go wrong. But we only log such timeouts, and
547 // invoke the listener with onCancelled.
Hans Boehmcc368502016-12-09 10:44:46 -0800548 timeout = NON_MAIN_TIMEOUT;
Hans Boehm8f051c32016-10-03 16:53:58 -0700549 }
Hans Boehm82e5a2f2015-07-20 20:08:14 -0700550 mTimeoutRunnable = new Runnable() {
551 @Override
552 public void run() {
Hans Boehm8f051c32016-10-03 16:53:58 -0700553 handleTimeout();
Hans Boehm82e5a2f2015-07-20 20:08:14 -0700554 }
555 };
Annie Chine5567fd2016-12-12 13:45:24 -0800556 mTimeoutHandler.removeCallbacks(mTimeoutRunnable);
Hans Boehm82e5a2f2015-07-20 20:08:14 -0700557 mTimeoutHandler.postDelayed(mTimeoutRunnable, timeout);
558 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700559
Hans Boehm3666e632015-07-27 18:33:12 -0700560 /**
561 * Is a computed result too big for decimal conversion?
562 */
Hans Boehm995e5eb2016-02-08 11:03:01 -0800563 private boolean isTooBig(UnifiedReal res) {
Hans Boehm83f278e2016-12-19 16:20:03 -0800564 final int maxBits = mRequired ? getMaxResultBits(mExprInfo.mLongTimeout)
Hans Boehm8f051c32016-10-03 16:53:58 -0700565 : QUICK_MAX_RESULT_BITS;
Hans Boehm995e5eb2016-02-08 11:03:01 -0800566 return res.approxWholeNumberBitsGreaterThan(maxBits);
Hans Boehm84614952014-11-25 18:46:17 -0800567 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700568
Hans Boehm84614952014-11-25 18:46:17 -0800569 @Override
570 protected InitialResult doInBackground(Void... nothing) {
571 try {
Hans Boehm8f051c32016-10-03 16:53:58 -0700572 // mExpr does not change while we are evaluating; thus it's OK to read here.
573 UnifiedReal res = mExprInfo.mVal.get();
574 if (res == null) {
Hans Boehm4452c782016-12-07 14:52:05 -0800575 try {
576 res = mExprInfo.mExpr.eval(mDm, Evaluator.this);
Hans Boehm64751002017-02-02 17:09:18 -0800577 if (isCancelled()) {
578 // TODO: This remains very slightly racey. Fix this.
579 throw new CR.AbortedException();
580 }
Hans Boehm4452c782016-12-07 14:52:05 -0800581 res = putResultIfAbsent(mIndex, res);
582 } catch (StackOverflowError e) {
583 // Absurdly large integer exponents can cause this. There might be other
584 // examples as well. Treat it as a timeout.
585 return new InitialResult(R.string.timeout);
586 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700587 }
Hans Boehm82e5a2f2015-07-20 20:08:14 -0700588 if (isTooBig(res)) {
589 // Avoid starting a long uninterruptible decimal conversion.
590 return new InitialResult(R.string.timeout);
591 }
Hans Boehm3666e632015-07-27 18:33:12 -0700592 int precOffset = INIT_PREC;
Hans Boehm995e5eb2016-02-08 11:03:01 -0800593 String initResult = res.toStringTruncated(precOffset);
Hans Boehm3666e632015-07-27 18:33:12 -0700594 int msd = getMsdIndexOf(initResult);
Hans Boehm995e5eb2016-02-08 11:03:01 -0800595 if (msd == INVALID_MSD) {
596 int leadingZeroBits = res.leadingBinaryZeroes();
597 if (leadingZeroBits < QUICK_MAX_RESULT_BITS) {
598 // Enough initial nonzero digits for most displays.
599 precOffset = 30 +
600 (int)Math.ceil(Math.log(2.0d) / Math.log(10.0d) * leadingZeroBits);
601 initResult = res.toStringTruncated(precOffset);
602 msd = getMsdIndexOf(initResult);
603 if (msd == INVALID_MSD) {
604 throw new AssertionError("Impossible zero result");
605 }
606 } else {
607 // Just try once more at higher fixed precision.
608 precOffset = MAX_MSD_PREC_OFFSET;
609 initResult = res.toStringTruncated(precOffset);
610 msd = getMsdIndexOf(initResult);
611 }
Hans Boehm84614952014-11-25 18:46:17 -0800612 }
Hans Boehm995e5eb2016-02-08 11:03:01 -0800613 final int lsdOffset = getLsdOffset(res, initResult, initResult.indexOf('.'));
Hans Boehm8f051c32016-10-03 16:53:58 -0700614 final int initDisplayOffset = getPreferredPrec(initResult, msd, lsdOffset,
615 mCharMetricsInfo);
Hans Boehm3666e632015-07-27 18:33:12 -0700616 final int newPrecOffset = initDisplayOffset + EXTRA_DIGITS;
617 if (newPrecOffset > precOffset) {
618 precOffset = newPrecOffset;
Hans Boehm995e5eb2016-02-08 11:03:01 -0800619 initResult = res.toStringTruncated(precOffset);
Hans Boehm84614952014-11-25 18:46:17 -0800620 }
Hans Boehm995e5eb2016-02-08 11:03:01 -0800621 return new InitialResult(res, initResult, precOffset, initDisplayOffset);
Hans Boehmc023b732015-04-29 11:30:47 -0700622 } catch (CalculatorExpr.SyntaxException e) {
Hans Boehm84614952014-11-25 18:46:17 -0800623 return new InitialResult(R.string.error_syntax);
Hans Boehm995e5eb2016-02-08 11:03:01 -0800624 } catch (UnifiedReal.ZeroDivisionException e) {
Hans Boehmfbcef702015-04-27 18:07:47 -0700625 return new InitialResult(R.string.error_zero_divide);
Hans Boehm84614952014-11-25 18:46:17 -0800626 } catch(ArithmeticException e) {
627 return new InitialResult(R.string.error_nan);
Hans Boehm19e93c92015-06-19 18:31:28 -0700628 } catch(CR.PrecisionOverflowException e) {
Hans Boehm3666e632015-07-27 18:33:12 -0700629 // Extremely unlikely unless we're actually dividing by zero or the like.
Hans Boehm84614952014-11-25 18:46:17 -0800630 return new InitialResult(R.string.error_overflow);
Hans Boehm19e93c92015-06-19 18:31:28 -0700631 } catch(CR.AbortedException e) {
Hans Boehm84614952014-11-25 18:46:17 -0800632 return new InitialResult(R.string.error_aborted);
633 }
634 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700635
Hans Boehm84614952014-11-25 18:46:17 -0800636 @Override
637 protected void onPostExecute(InitialResult result) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700638 mExprInfo.mEvaluator = null;
Hans Boehm84614952014-11-25 18:46:17 -0800639 mTimeoutHandler.removeCallbacks(mTimeoutRunnable);
640 if (result.isError()) {
Hans Boehm3666e632015-07-27 18:33:12 -0700641 if (result.errorResourceId == R.string.timeout) {
Hans Boehm8f051c32016-10-03 16:53:58 -0700642 // Emulating timeout due to large result.
643 if (mRequired && mIndex == MAIN_INDEX) {
644 displayTimeoutMessage(mExprs.get(mIndex).mLongTimeout);
Hans Boehm82e5a2f2015-07-20 20:08:14 -0700645 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700646 mListener.onCancelled(mIndex);
Hans Boehm82e5a2f2015-07-20 20:08:14 -0700647 } else {
Hans Boehm03b34512016-12-13 11:32:20 -0800648 if (mRequired) {
649 mExprInfo.mResultString = ERRONEOUS_RESULT;
650 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700651 mListener.onError(mIndex, result.errorResourceId);
Hans Boehm82e5a2f2015-07-20 20:08:14 -0700652 }
Hans Boehm84614952014-11-25 18:46:17 -0800653 return;
654 }
Hans Boehm758cb2e2016-11-14 14:49:28 -0800655 // mExprInfo.mVal was already set asynchronously by child thread.
Hans Boehm8f051c32016-10-03 16:53:58 -0700656 mExprInfo.mResultString = result.newResultString;
657 mExprInfo.mResultStringOffset = result.newResultStringOffset;
658 final int dotIndex = mExprInfo.mResultString.indexOf('.');
659 String truncatedWholePart = mExprInfo.mResultString.substring(0, dotIndex);
Hans Boehm3666e632015-07-27 18:33:12 -0700660 // Recheck display precision; it may change, since display dimensions may have been
661 // unknow the first time. In that case the initial evaluation precision should have
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700662 // been conservative.
Hans Boehm3666e632015-07-27 18:33:12 -0700663 // TODO: Could optimize by remembering display size and checking for change.
664 int initPrecOffset = result.initDisplayOffset;
Hans Boehm8f051c32016-10-03 16:53:58 -0700665 mExprInfo.mMsdIndex = getMsdIndexOf(mExprInfo.mResultString);
666 final int leastDigOffset = getLsdOffset(result.val, mExprInfo.mResultString,
667 dotIndex);
668 final int newInitPrecOffset = getPreferredPrec(mExprInfo.mResultString,
669 mExprInfo.mMsdIndex, leastDigOffset, mCharMetricsInfo);
Hans Boehm3666e632015-07-27 18:33:12 -0700670 if (newInitPrecOffset < initPrecOffset) {
671 initPrecOffset = newInitPrecOffset;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700672 } else {
Hans Boehm3666e632015-07-27 18:33:12 -0700673 // They should be equal. But nothing horrible should happen if they're not. e.g.
674 // because CalculatorResult.MAX_WIDTH was too small.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700675 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700676 mListener.onEvaluate(mIndex, initPrecOffset, mExprInfo.mMsdIndex, leastDigOffset,
677 truncatedWholePart);
Hans Boehm84614952014-11-25 18:46:17 -0800678 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700679
Hans Boehm84614952014-11-25 18:46:17 -0800680 @Override
681 protected void onCancelled(InitialResult result) {
Hans Boehm82e5a2f2015-07-20 20:08:14 -0700682 // Invoker resets mEvaluator.
683 mTimeoutHandler.removeCallbacks(mTimeoutRunnable);
Hans Boehm8f051c32016-10-03 16:53:58 -0700684 if (!mQuiet) {
Hans Boehmc023b732015-04-29 11:30:47 -0700685 displayCancelledMessage();
Hans Boehm3666e632015-07-27 18:33:12 -0700686 } // Otherwise, if mRequired, timeout processing displayed message.
Hans Boehm8f051c32016-10-03 16:53:58 -0700687 mListener.onCancelled(mIndex);
Hans Boehm84614952014-11-25 18:46:17 -0800688 // Just drop the evaluation; Leave expression displayed.
689 return;
690 }
691 }
692
Hans Boehm3666e632015-07-27 18:33:12 -0700693 /**
Hans Boehm79302b02015-08-05 17:20:32 -0700694 * Check whether a new higher precision result flips previously computed trailing 9s
695 * to zeroes. If so, flip them back. Return the adjusted result.
696 * Assumes newPrecOffset >= oldPrecOffset > 0.
697 * Since our results are accurate to < 1 ulp, this can only happen if the true result
698 * is less than the new result with trailing zeroes, and thus appending 9s to the
699 * old result must also be correct. Such flips are impossible if the newly computed
700 * digits consist of anything other than zeroes.
701 * It is unclear that there are real cases in which this is necessary,
702 * but we have failed to prove there aren't such cases.
703 */
704 @VisibleForTesting
Annie Chin56bcbf12016-09-23 17:04:22 -0700705 public static String unflipZeroes(String oldDigs, int oldPrecOffset, String newDigs,
Hans Boehm79302b02015-08-05 17:20:32 -0700706 int newPrecOffset) {
707 final int oldLen = oldDigs.length();
708 if (oldDigs.charAt(oldLen - 1) != '9') {
709 return newDigs;
710 }
711 final int newLen = newDigs.length();
712 final int precDiff = newPrecOffset - oldPrecOffset;
713 final int oldLastInNew = newLen - 1 - precDiff;
714 if (newDigs.charAt(oldLastInNew) != '0') {
715 return newDigs;
716 }
717 // Earlier digits could not have changed without a 0 to 9 or 9 to 0 flip at end.
718 // The former is OK.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700719 if (!newDigs.substring(newLen - precDiff).equals(StringUtils.repeat('0', precDiff))) {
Hans Boehm79302b02015-08-05 17:20:32 -0700720 throw new AssertionError("New approximation invalidates old one!");
721 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700722 return oldDigs + StringUtils.repeat('9', precDiff);
Hans Boehm79302b02015-08-05 17:20:32 -0700723 }
724
725 /**
Hans Boehm3666e632015-07-27 18:33:12 -0700726 * Result of asynchronous reevaluation.
727 */
728 private static class ReevalResult {
729 public final String newResultString;
730 public final int newResultStringOffset;
731 ReevalResult(String s, int p) {
732 newResultString = s;
733 newResultStringOffset = p;
734 }
735 }
Hans Boehm84614952014-11-25 18:46:17 -0800736
Hans Boehm3666e632015-07-27 18:33:12 -0700737 /**
738 * Compute new mResultString contents to prec digits to the right of the decimal point.
739 * Ensure that onReevaluate() is called after doing so. If the evaluation fails for reasons
740 * other than a timeout, ensure that onError() is called.
Hans Boehm8f051c32016-10-03 16:53:58 -0700741 * This assumes that initial evaluation of the expression has been successfully
742 * completed.
Hans Boehm3666e632015-07-27 18:33:12 -0700743 */
744 private class AsyncReevaluator extends AsyncTask<Integer, Void, ReevalResult> {
Hans Boehm8f051c32016-10-03 16:53:58 -0700745 private long mIndex; // Index of expression to evaluate.
746 private EvaluationListener mListener;
747 private ExprInfo mExprInfo;
748
749 AsyncReevaluator(long index, EvaluationListener listener) {
750 mIndex = index;
751 mListener = listener;
752 mExprInfo = mExprs.get(mIndex);
753 }
754
Hans Boehm3666e632015-07-27 18:33:12 -0700755 @Override
756 protected ReevalResult doInBackground(Integer... prec) {
757 try {
758 final int precOffset = prec[0].intValue();
Hans Boehm8f051c32016-10-03 16:53:58 -0700759 return new ReevalResult(mExprInfo.mVal.get().toStringTruncated(precOffset),
760 precOffset);
Hans Boehm3666e632015-07-27 18:33:12 -0700761 } catch(ArithmeticException e) {
762 return null;
763 } catch(CR.PrecisionOverflowException e) {
764 return null;
765 } catch(CR.AbortedException e) {
766 // Should only happen if the task was cancelled, in which case we don't look at
767 // the result.
768 return null;
769 }
770 }
Hans Boehm79302b02015-08-05 17:20:32 -0700771
Hans Boehm3666e632015-07-27 18:33:12 -0700772 @Override
773 protected void onPostExecute(ReevalResult result) {
774 if (result == null) {
775 // This should only be possible in the extremely rare case of encountering a
776 // domain error while reevaluating or in case of a precision overflow. We don't
777 // know of a way to get the latter with a plausible amount of user input.
Hans Boehm849d8a42016-12-12 18:19:55 -0800778 mExprInfo.mResultString = ERRONEOUS_RESULT;
Hans Boehm8f051c32016-10-03 16:53:58 -0700779 mListener.onError(mIndex, R.string.error_nan);
Hans Boehm3666e632015-07-27 18:33:12 -0700780 } else {
Hans Boehm8f051c32016-10-03 16:53:58 -0700781 if (result.newResultStringOffset < mExprInfo.mResultStringOffset) {
Hans Boehm3666e632015-07-27 18:33:12 -0700782 throw new AssertionError("Unexpected onPostExecute timing");
783 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700784 mExprInfo.mResultString = unflipZeroes(mExprInfo.mResultString,
785 mExprInfo.mResultStringOffset, result.newResultString,
786 result.newResultStringOffset);
787 mExprInfo.mResultStringOffset = result.newResultStringOffset;
788 mListener.onReevaluate(mIndex);
Hans Boehm3666e632015-07-27 18:33:12 -0700789 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700790 mExprInfo.mEvaluator = null;
Hans Boehm3666e632015-07-27 18:33:12 -0700791 }
792 // On cancellation we do nothing; invoker should have left no trace of us.
793 }
794
795 /**
Hans Boehm8f051c32016-10-03 16:53:58 -0700796 * If necessary, start an evaluation of the expression at the given index to precOffset.
797 * If we start an evaluation the listener is notified on completion.
Hans Boehm849d8a42016-12-12 18:19:55 -0800798 * Only called if prior evaluation succeeded.
Hans Boehm3666e632015-07-27 18:33:12 -0700799 */
Hans Boehm8f051c32016-10-03 16:53:58 -0700800 private void ensureCachePrec(long index, int precOffset, EvaluationListener listener) {
801 ExprInfo ei = mExprs.get(index);
802 if (ei.mResultString != null && ei.mResultStringOffset >= precOffset
803 || ei.mResultStringOffsetReq >= precOffset) return;
804 if (ei.mEvaluator != null) {
Hans Boehm84614952014-11-25 18:46:17 -0800805 // Ensure we only have one evaluation running at a time.
Hans Boehm8f051c32016-10-03 16:53:58 -0700806 ei.mEvaluator.cancel(true);
807 ei.mEvaluator = null;
Hans Boehm84614952014-11-25 18:46:17 -0800808 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700809 AsyncReevaluator reEval = new AsyncReevaluator(index, listener);
810 ei.mEvaluator = reEval;
811 ei.mResultStringOffsetReq = precOffset + PRECOMPUTE_DIGITS;
812 if (ei.mResultString != null) {
813 ei.mResultStringOffsetReq += ei.mResultStringOffsetReq / PRECOMPUTE_DIVISOR;
Hans Boehme4579122015-05-26 17:52:32 -0700814 }
Hans Boehm8f051c32016-10-03 16:53:58 -0700815 reEval.execute(ei.mResultStringOffsetReq);
Hans Boehm84614952014-11-25 18:46:17 -0800816 }
817
Hans Boehma0e45f32015-05-30 13:20:35 -0700818 /**
819 * Return the rightmost nonzero digit position, if any.
Hans Boehm995e5eb2016-02-08 11:03:01 -0800820 * @param val UnifiedReal value of result.
Hans Boehma0e45f32015-05-30 13:20:35 -0700821 * @param cache Current cached decimal string representation of result.
Hans Boehm3666e632015-07-27 18:33:12 -0700822 * @param decIndex Index of decimal point in cache.
Hans Boehma0e45f32015-05-30 13:20:35 -0700823 * @result Position of rightmost nonzero digit relative to decimal point.
Hans Boehm995e5eb2016-02-08 11:03:01 -0800824 * Integer.MIN_VALUE if we cannot determine. Integer.MAX_VALUE if there is no lsd,
Hans Boehma0e45f32015-05-30 13:20:35 -0700825 * or we cannot determine it.
826 */
Hans Boehm8f051c32016-10-03 16:53:58 -0700827 static int getLsdOffset(UnifiedReal val, String cache, int decIndex) {
Hans Boehm995e5eb2016-02-08 11:03:01 -0800828 if (val.definitelyZero()) return Integer.MIN_VALUE;
829 int result = val.digitsRequired();
Hans Boehma0e45f32015-05-30 13:20:35 -0700830 if (result == 0) {
831 int i;
Hans Boehm3666e632015-07-27 18:33:12 -0700832 for (i = -1; decIndex + i > 0 && cache.charAt(decIndex + i) == '0'; --i) { }
Hans Boehma0e45f32015-05-30 13:20:35 -0700833 result = i;
834 }
835 return result;
836 }
837
Hans Boehm79302b02015-08-05 17:20:32 -0700838 // TODO: We may want to consistently specify the position of the current result
839 // window using the left-most visible digit index instead of the offset for the rightmost one.
840 // It seems likely that would simplify the logic.
841
Hans Boehma0e45f32015-05-30 13:20:35 -0700842 /**
Hans Boehm79302b02015-08-05 17:20:32 -0700843 * Retrieve the preferred precision "offset" for the currently displayed result.
Hans Boehma0e45f32015-05-30 13:20:35 -0700844 * May be called from non-UI thread.
845 * @param cache Current approximation as string.
846 * @param msd Position of most significant digit in result. Index in cache.
847 * Can be INVALID_MSD if we haven't found it yet.
Hans Boehm3666e632015-07-27 18:33:12 -0700848 * @param lastDigitOffset Position of least significant digit (1 = tenths digit)
Hans Boehma0e45f32015-05-30 13:20:35 -0700849 * or Integer.MAX_VALUE.
850 */
Hans Boehm8f051c32016-10-03 16:53:58 -0700851 private static int getPreferredPrec(String cache, int msd, int lastDigitOffset,
852 CharMetricsInfo cm) {
853 final int lineLength = cm.getMaxChars();
Hans Boehm3666e632015-07-27 18:33:12 -0700854 final int wholeSize = cache.indexOf('.');
Hans Boehm8f051c32016-10-03 16:53:58 -0700855 final float rawSepChars = cm.separatorChars(cache, wholeSize);
856 final float rawSepCharsNoDecimal = rawSepChars - cm.getNoEllipsisCredit();
857 final float rawSepCharsWithDecimal = rawSepCharsNoDecimal - cm.getDecimalCredit();
Hans Boehm24c91ed2016-06-30 18:53:44 -0700858 final int sepCharsNoDecimal = (int) Math.ceil(Math.max(rawSepCharsNoDecimal, 0.0f));
859 final int sepCharsWithDecimal = (int) Math.ceil(Math.max(rawSepCharsWithDecimal, 0.0f));
Hans Boehm3666e632015-07-27 18:33:12 -0700860 final int negative = cache.charAt(0) == '-' ? 1 : 0;
Hans Boehm682ff5e2015-03-09 14:40:25 -0700861 // Don't display decimal point if result is an integer.
Hans Boehm3666e632015-07-27 18:33:12 -0700862 if (lastDigitOffset == 0) {
863 lastDigitOffset = -1;
864 }
865 if (lastDigitOffset != Integer.MAX_VALUE) {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700866 if (wholeSize <= lineLength - sepCharsNoDecimal && lastDigitOffset <= 0) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700867 // Exact integer. Prefer to display as integer, without decimal point.
868 return -1;
869 }
Hans Boehm3666e632015-07-27 18:33:12 -0700870 if (lastDigitOffset >= 0
Hans Boehm24c91ed2016-06-30 18:53:44 -0700871 && wholeSize + lastDigitOffset + 1 /* decimal pt. */
872 <= lineLength - sepCharsWithDecimal) {
873 // Display full exact number without scientific notation.
Hans Boehm3666e632015-07-27 18:33:12 -0700874 return lastDigitOffset;
Hans Boehma0e45f32015-05-30 13:20:35 -0700875 }
Hans Boehm84614952014-11-25 18:46:17 -0800876 }
Hans Boehm50ed3202015-06-09 14:35:49 -0700877 if (msd > wholeSize && msd <= wholeSize + EXP_COST + 1) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700878 // Display number without scientific notation. Treat leading zero as msd.
Hans Boehm84614952014-11-25 18:46:17 -0800879 msd = wholeSize - 1;
880 }
Hans Boehm995e5eb2016-02-08 11:03:01 -0800881 if (msd > QUICK_MAX_RESULT_BITS) {
882 // Display a probable but uncertain 0 as "0.000000000", without exponent. That's a
883 // judgment call, but less likely to confuse naive users. A more informative and
884 // confusing option would be to use a large negative exponent.
885 // Treat extremely large msd values as unknown to avoid slow computations.
Hans Boehm760a9dc2015-04-20 10:27:12 -0700886 return lineLength - 2;
887 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700888 // Return position corresponding to having msd at left, effectively presuming scientific
889 // notation that preserves the left part of the result.
890 // After adjustment for the space required by an exponent, evaluating to the resulting
891 // precision should not overflow the display.
892 int result = msd - wholeSize + lineLength - negative - 1;
893 if (wholeSize <= lineLength - sepCharsNoDecimal) {
894 // Fits without scientific notation; will need space for separators.
895 if (wholeSize < lineLength - sepCharsWithDecimal) {
896 result -= sepCharsWithDecimal;
897 } else {
898 result -= sepCharsNoDecimal;
899 }
900 }
901 return result;
Hans Boehm84614952014-11-25 18:46:17 -0800902 }
903
Hans Boehm50ed3202015-06-09 14:35:49 -0700904 private static final int SHORT_TARGET_LENGTH = 8;
905 private static final String SHORT_UNCERTAIN_ZERO = "0.00000" + KeyMaps.ELLIPSIS;
Hans Boehm013969e2015-04-13 20:29:47 -0700906
Hans Boehm50ed3202015-06-09 14:35:49 -0700907 /**
908 * Get a short representation of the value represented by the string cache.
909 * We try to match the CalculatorResult code when the result is finite
910 * and small enough to suit our needs.
911 * The result is not internationalized.
912 * @param cache String approximation of value. Assumed to be long enough
913 * that if it doesn't contain enough significant digits, we can
914 * reasonably abbreviate as SHORT_UNCERTAIN_ZERO.
915 * @param msdIndex Index of most significant digit in cache, or INVALID_MSD.
Hans Boehm3666e632015-07-27 18:33:12 -0700916 * @param lsdOffset Position of least significant digit in finite representation,
Hans Boehm50ed3202015-06-09 14:35:49 -0700917 * relative to decimal point, or MAX_VALUE.
918 */
Hans Boehm8f051c32016-10-03 16:53:58 -0700919 private static String getShortString(String cache, int msdIndex, int lsdOffset) {
Hans Boehm50ed3202015-06-09 14:35:49 -0700920 // This somewhat mirrors the display formatting code, but
921 // - The constants are different, since we don't want to use the whole display.
922 // - This is an easier problem, since we don't support scrolling and the length
923 // is a bit flexible.
924 // TODO: Think about refactoring this to remove partial redundancy with CalculatorResult.
925 final int dotIndex = cache.indexOf('.');
926 final int negative = cache.charAt(0) == '-' ? 1 : 0;
927 final String negativeSign = negative == 1 ? "-" : "";
928
929 // Ensure we don't have to worry about running off the end of cache.
930 if (msdIndex >= cache.length() - SHORT_TARGET_LENGTH) {
931 msdIndex = INVALID_MSD;
932 }
933 if (msdIndex == INVALID_MSD) {
Hans Boehm3666e632015-07-27 18:33:12 -0700934 if (lsdOffset < INIT_PREC) {
Hans Boehm50ed3202015-06-09 14:35:49 -0700935 return "0";
936 } else {
937 return SHORT_UNCERTAIN_ZERO;
Hans Boehm84614952014-11-25 18:46:17 -0800938 }
Hans Boehm84614952014-11-25 18:46:17 -0800939 }
Hans Boehm50ed3202015-06-09 14:35:49 -0700940 // Avoid scientific notation for small numbers of zeros.
941 // Instead stretch significant digits to include decimal point.
Hans Boehm3666e632015-07-27 18:33:12 -0700942 if (lsdOffset < -1 && dotIndex - msdIndex + negative <= SHORT_TARGET_LENGTH
943 && lsdOffset >= -CalculatorResult.MAX_TRAILING_ZEROES - 1) {
Hans Boehm50ed3202015-06-09 14:35:49 -0700944 // Whole number that fits in allotted space.
945 // CalculatorResult would not use scientific notation either.
Hans Boehm3666e632015-07-27 18:33:12 -0700946 lsdOffset = -1;
Hans Boehm013969e2015-04-13 20:29:47 -0700947 }
Hans Boehm50ed3202015-06-09 14:35:49 -0700948 if (msdIndex > dotIndex) {
949 if (msdIndex <= dotIndex + EXP_COST + 1) {
Hans Boehm762266e2016-07-25 13:37:09 -0700950 // Preferred display format in this case is with leading zeroes, even if
Hans Boehm50ed3202015-06-09 14:35:49 -0700951 // it doesn't fit entirely. Replicate that here.
952 msdIndex = dotIndex - 1;
Hans Boehm3666e632015-07-27 18:33:12 -0700953 } else if (lsdOffset <= SHORT_TARGET_LENGTH - negative - 2
954 && lsdOffset <= CalculatorResult.MAX_LEADING_ZEROES + 1) {
Hans Boehm50ed3202015-06-09 14:35:49 -0700955 // Fraction that fits entirely in allotted space.
956 // CalculatorResult would not use scientific notation either.
957 msdIndex = dotIndex -1;
958 }
959 }
960 int exponent = dotIndex - msdIndex;
961 if (exponent > 0) {
962 // Adjust for the fact that the decimal point itself takes space.
963 exponent--;
964 }
Hans Boehm3666e632015-07-27 18:33:12 -0700965 if (lsdOffset != Integer.MAX_VALUE) {
966 final int lsdIndex = dotIndex + lsdOffset;
967 final int totalDigits = lsdIndex - msdIndex + negative + 1;
968 if (totalDigits <= SHORT_TARGET_LENGTH && dotIndex > msdIndex && lsdOffset >= -1) {
Hans Boehm50ed3202015-06-09 14:35:49 -0700969 // Fits, no exponent needed.
Hans Boehm762266e2016-07-25 13:37:09 -0700970 final String wholeWithCommas = StringUtils.addCommas(cache, msdIndex, dotIndex);
971 return negativeSign + wholeWithCommas + cache.substring(dotIndex, lsdIndex + 1);
Hans Boehm50ed3202015-06-09 14:35:49 -0700972 }
973 if (totalDigits <= SHORT_TARGET_LENGTH - 3) {
974 return negativeSign + cache.charAt(msdIndex) + "."
Hans Boehm0b9806f2015-06-29 16:07:15 -0700975 + cache.substring(msdIndex + 1, lsdIndex + 1) + "E" + exponent;
Hans Boehm50ed3202015-06-09 14:35:49 -0700976 }
977 }
978 // We need to abbreviate.
979 if (dotIndex > msdIndex && dotIndex < msdIndex + SHORT_TARGET_LENGTH - negative - 1) {
Hans Boehm762266e2016-07-25 13:37:09 -0700980 final String wholeWithCommas = StringUtils.addCommas(cache, msdIndex, dotIndex);
981 return negativeSign + wholeWithCommas
982 + cache.substring(dotIndex, msdIndex + SHORT_TARGET_LENGTH - negative - 1)
983 + KeyMaps.ELLIPSIS;
Hans Boehm50ed3202015-06-09 14:35:49 -0700984 }
985 // Need abbreviation + exponent
986 return negativeSign + cache.charAt(msdIndex) + "."
987 + cache.substring(msdIndex + 1, msdIndex + SHORT_TARGET_LENGTH - negative - 4)
Hans Boehm0b9806f2015-06-29 16:07:15 -0700988 + KeyMaps.ELLIPSIS + "E" + exponent;
Hans Boehm84614952014-11-25 18:46:17 -0800989 }
990
Hans Boehm3666e632015-07-27 18:33:12 -0700991 /**
992 * Return the most significant digit index in the given numeric string.
993 * Return INVALID_MSD if there are not enough digits to prove the numeric value is
994 * different from zero. As usual, we assume an error of strictly less than 1 ulp.
995 */
996 public static int getMsdIndexOf(String s) {
997 final int len = s.length();
998 int nonzeroIndex = -1;
Hans Boehm84614952014-11-25 18:46:17 -0800999 for (int i = 0; i < len; ++i) {
1000 char c = s.charAt(i);
1001 if (c != '-' && c != '.' && c != '0') {
Hans Boehm3666e632015-07-27 18:33:12 -07001002 nonzeroIndex = i;
Hans Boehm84614952014-11-25 18:46:17 -08001003 break;
1004 }
1005 }
Hans Boehm3666e632015-07-27 18:33:12 -07001006 if (nonzeroIndex >= 0 && (nonzeroIndex < len - 1 || s.charAt(nonzeroIndex) != '1')) {
1007 return nonzeroIndex;
Hans Boehm84614952014-11-25 18:46:17 -08001008 } else {
Hans Boehm84614952014-11-25 18:46:17 -08001009 return INVALID_MSD;
1010 }
Hans Boehm84614952014-11-25 18:46:17 -08001011 }
1012
Hans Boehm3666e632015-07-27 18:33:12 -07001013 /**
Hans Boehm8f051c32016-10-03 16:53:58 -07001014 * Return most significant digit index for the result of the expressin at the given index.
Hans Boehm3666e632015-07-27 18:33:12 -07001015 * Returns an index in the result character array. Return INVALID_MSD if the current result
1016 * is too close to zero to determine the result.
Hans Boehm79302b02015-08-05 17:20:32 -07001017 * Result is almost consistent through reevaluations: It may increase by one, once.
Hans Boehm3666e632015-07-27 18:33:12 -07001018 */
Hans Boehm8f051c32016-10-03 16:53:58 -07001019 private int getMsdIndex(long index) {
1020 ExprInfo ei = mExprs.get(index);
1021 if (ei.mMsdIndex != INVALID_MSD) {
Hans Boehm79302b02015-08-05 17:20:32 -07001022 // 0.100000... can change to 0.0999999... We may have to correct once by one digit.
Hans Boehm8f051c32016-10-03 16:53:58 -07001023 if (ei.mResultString.charAt(ei.mMsdIndex) == '0') {
1024 ei.mMsdIndex++;
Hans Boehm79302b02015-08-05 17:20:32 -07001025 }
Hans Boehm8f051c32016-10-03 16:53:58 -07001026 return ei.mMsdIndex;
Hans Boehm79302b02015-08-05 17:20:32 -07001027 }
Hans Boehm8f051c32016-10-03 16:53:58 -07001028 if (ei.mVal.get().definitelyZero()) {
Hans Boehm84614952014-11-25 18:46:17 -08001029 return INVALID_MSD; // None exists
1030 }
Hans Boehm3666e632015-07-27 18:33:12 -07001031 int result = INVALID_MSD;
Hans Boehm8f051c32016-10-03 16:53:58 -07001032 if (ei.mResultString != null) {
1033 result = ei.mMsdIndex = getMsdIndexOf(ei.mResultString);
Hans Boehm84614952014-11-25 18:46:17 -08001034 }
Hans Boehm3666e632015-07-27 18:33:12 -07001035 return result;
Hans Boehm84614952014-11-25 18:46:17 -08001036 }
1037
Hans Boehm3666e632015-07-27 18:33:12 -07001038 // Refuse to scroll past the point at which this many digits from the whole number
1039 // part of the result are still displayed. Avoids sily displays like 1E1.
1040 private static final int MIN_DISPLAYED_DIGS = 5;
1041
1042 /**
1043 * Return result to precOffset[0] digits to the right of the decimal point.
1044 * PrecOffset[0] is updated if the original value is out of range. No exponent or other
1045 * indication of precision is added. The result is returned immediately, based on the current
Hans Boehm8f051c32016-10-03 16:53:58 -07001046 * cache contents, but it may contain blanks for unknown digits. It may also use
Hans Boehm3666e632015-07-27 18:33:12 -07001047 * uncertain digits within EXTRA_DIGITS. If either of those occurred, schedule a reevaluation
1048 * and redisplay operation. Uncertain digits never appear to the left of the decimal point.
1049 * PrecOffset[0] may be negative to only retrieve digits to the left of the decimal point.
1050 * (precOffset[0] = 0 means we include the decimal point, but nothing to the right.
1051 * precOffset[0] = -1 means we drop the decimal point and start at the ones position. Should
1052 * not be invoked before the onEvaluate() callback is received. This essentially just returns
1053 * a substring of the full result; a leading minus sign or leading digits can be dropped.
Hans Boehm995e5eb2016-02-08 11:03:01 -08001054 * Result uses US conventions; is NOT internationalized. Use getResult() and UnifiedReal
1055 * operations to determine whether the result is exact, or whether we dropped trailing digits.
Hans Boehm3666e632015-07-27 18:33:12 -07001056 *
Hans Boehm8f051c32016-10-03 16:53:58 -07001057 * @param index Index of expression to approximate
Hans Boehm3666e632015-07-27 18:33:12 -07001058 * @param precOffset Zeroth element indicates desired and actual precision
1059 * @param maxPrecOffset Maximum adjusted precOffset[0]
Hans Boehm79302b02015-08-05 17:20:32 -07001060 * @param maxDigs Maximum length of result
Hans Boehm3666e632015-07-27 18:33:12 -07001061 * @param truncated Zeroth element is set if leading nonzero digits were dropped
1062 * @param negative Zeroth element is set of the result is negative.
Hans Boehm8f051c32016-10-03 16:53:58 -07001063 * @param listener EvaluationListener to notify when reevaluation is complete.
Hans Boehm3666e632015-07-27 18:33:12 -07001064 */
Hans Boehm8f051c32016-10-03 16:53:58 -07001065 public String getString(long index, int[] precOffset, int maxPrecOffset, int maxDigs,
1066 boolean[] truncated, boolean[] negative, EvaluationListener listener) {
1067 ExprInfo ei = mExprs.get(index);
Hans Boehm3666e632015-07-27 18:33:12 -07001068 int currentPrecOffset = precOffset[0];
Hans Boehm84614952014-11-25 18:46:17 -08001069 // Make sure we eventually get a complete answer
Hans Boehm8f051c32016-10-03 16:53:58 -07001070 if (ei.mResultString == null) {
1071 ensureCachePrec(index, currentPrecOffset + EXTRA_DIGITS, listener);
Hans Boehm3666e632015-07-27 18:33:12 -07001072 // Nothing else to do now; seems to happen on rare occasion with weird user input
1073 // timing; Will repair itself in a jiffy.
Hans Boehm79302b02015-08-05 17:20:32 -07001074 return " ";
Hans Boehm3666e632015-07-27 18:33:12 -07001075 } else {
Hans Boehm8f051c32016-10-03 16:53:58 -07001076 ensureCachePrec(index, currentPrecOffset + EXTRA_DIGITS + ei.mResultString.length()
1077 / EXTRA_DIVISOR, listener);
Hans Boehm3666e632015-07-27 18:33:12 -07001078 }
1079 // Compute an appropriate substring of mResultString. Pad if necessary.
Hans Boehm8f051c32016-10-03 16:53:58 -07001080 final int len = ei.mResultString.length();
1081 final boolean myNegative = ei.mResultString.charAt(0) == '-';
Hans Boehm3666e632015-07-27 18:33:12 -07001082 negative[0] = myNegative;
1083 // Don't scroll left past leftmost digits in mResultString unless that still leaves an
1084 // integer.
Hans Boehm8f051c32016-10-03 16:53:58 -07001085 int integralDigits = len - ei.mResultStringOffset;
Hans Boehm3666e632015-07-27 18:33:12 -07001086 // includes 1 for dec. pt
1087 if (myNegative) {
1088 --integralDigits;
Hans Boehm84614952014-11-25 18:46:17 -08001089 }
Hans Boehm3666e632015-07-27 18:33:12 -07001090 int minPrecOffset = Math.min(MIN_DISPLAYED_DIGS - integralDigits, -1);
1091 currentPrecOffset = Math.min(Math.max(currentPrecOffset, minPrecOffset),
1092 maxPrecOffset);
1093 precOffset[0] = currentPrecOffset;
Hans Boehm8f051c32016-10-03 16:53:58 -07001094 int extraDigs = ei.mResultStringOffset - currentPrecOffset; // trailing digits to drop
Hans Boehm3666e632015-07-27 18:33:12 -07001095 int deficit = 0; // The number of digits we're short
1096 if (extraDigs < 0) {
1097 extraDigs = 0;
Hans Boehm8f051c32016-10-03 16:53:58 -07001098 deficit = Math.min(currentPrecOffset - ei.mResultStringOffset, maxDigs);
Hans Boehm3666e632015-07-27 18:33:12 -07001099 }
1100 int endIndex = len - extraDigs;
1101 if (endIndex < 1) {
1102 return " ";
1103 }
1104 int startIndex = Math.max(endIndex + deficit - maxDigs, 0);
Hans Boehm8f051c32016-10-03 16:53:58 -07001105 truncated[0] = (startIndex > getMsdIndex(index));
1106 String result = ei.mResultString.substring(startIndex, endIndex);
Hans Boehm3666e632015-07-27 18:33:12 -07001107 if (deficit > 0) {
Hans Boehm24c91ed2016-06-30 18:53:44 -07001108 result += StringUtils.repeat(' ', deficit);
Hans Boehm79302b02015-08-05 17:20:32 -07001109 // Blank character is replaced during translation.
Hans Boehm3666e632015-07-27 18:33:12 -07001110 // Since we always compute past the decimal point, this never fills in the spot
1111 // where the decimal point should go, and we can otherwise treat placeholders
1112 // as though they were digits.
1113 }
1114 return result;
Hans Boehm84614952014-11-25 18:46:17 -08001115 }
1116
Hans Boehm3666e632015-07-27 18:33:12 -07001117 /**
Hans Boehm8f051c32016-10-03 16:53:58 -07001118 * Clear the cache for the main expression.
Hans Boehm3666e632015-07-27 18:33:12 -07001119 */
Hans Boehm8f051c32016-10-03 16:53:58 -07001120 private void clearMainCache() {
1121 mMainExpr.mVal.set(null);
1122 mMainExpr.mResultString = null;
1123 mMainExpr.mResultStringOffset = mMainExpr.mResultStringOffsetReq = 0;
1124 mMainExpr.mMsdIndex = INVALID_MSD;
Hans Boehm84614952014-11-25 18:46:17 -08001125 }
1126
Hans Boehm5e6a0ca2015-09-22 17:09:01 -07001127
Hans Boehmcc368502016-12-09 10:44:46 -08001128 public void clearMain() {
Hans Boehm8f051c32016-10-03 16:53:58 -07001129 mMainExpr.mExpr.clear();
Hans Boehm52d477a2016-04-01 17:42:50 -07001130 mHasTrigFuncs = false;
Hans Boehm8f051c32016-10-03 16:53:58 -07001131 clearMainCache();
Hans Boehm8f051c32016-10-03 16:53:58 -07001132 mMainExpr.mLongTimeout = false;
1133 }
1134
1135 public void clearEverything() {
1136 boolean dm = mMainExpr.mDegreeMode;
1137 cancelAll(true);
1138 setSavedIndex(0);
1139 setMemoryIndex(0);
1140 mExprDB.eraseAll();
1141 mExprs.clear();
1142 setMainExpr(new ExprInfo(new CalculatorExpr(), dm));
Hans Boehm5e6a0ca2015-09-22 17:09:01 -07001143 }
1144
Hans Boehmae807e12015-07-14 15:40:52 -07001145 /**
Hans Boehm8f051c32016-10-03 16:53:58 -07001146 * Start asynchronous evaluation.
1147 * Invoke listener on successful completion. If the result is required, invoke
1148 * onCancelled() if cancelled.
1149 * @param index index of expression to be evaluated.
Hans Boehmae807e12015-07-14 15:40:52 -07001150 * @param required result was explicitly requested by user.
1151 */
Hans Boehm8f051c32016-10-03 16:53:58 -07001152 private void evaluateResult(long index, EvaluationListener listener, CharMetricsInfo cmi,
1153 boolean required) {
1154 ExprInfo ei = mExprs.get(index);
Hans Boehm758cb2e2016-11-14 14:49:28 -08001155 if (index == MAIN_INDEX) {
1156 clearMainCache();
1157 } // Otherwise the expression is immutable.
Hans Boehm8f051c32016-10-03 16:53:58 -07001158 AsyncEvaluator eval = new AsyncEvaluator(index, listener, cmi, ei.mDegreeMode, required);
1159 ei.mEvaluator = eval;
1160 eval.execute();
1161 if (index == MAIN_INDEX) {
1162 mChangedValue = false;
Hans Boehmc023b732015-04-29 11:30:47 -07001163 }
Hans Boehm84614952014-11-25 18:46:17 -08001164 }
1165
Hans Boehm3666e632015-07-27 18:33:12 -07001166 /**
Hans Boehm8f051c32016-10-03 16:53:58 -07001167 * Notify listener of a previously completed evaluation.
Hans Boehm79302b02015-08-05 17:20:32 -07001168 */
Hans Boehm8f051c32016-10-03 16:53:58 -07001169 void notifyImmediately(long index, ExprInfo ei, EvaluationListener listener,
1170 CharMetricsInfo cmi) {
1171 final int dotIndex = ei.mResultString.indexOf('.');
1172 final String truncatedWholePart = ei.mResultString.substring(0, dotIndex);
1173 final int leastDigOffset = getLsdOffset(ei.mVal.get(), ei.mResultString, dotIndex);
1174 final int msdIndex = getMsdIndex(index);
1175 final int preferredPrecOffset = getPreferredPrec(ei.mResultString, msdIndex,
1176 leastDigOffset, cmi);
1177 listener.onEvaluate(index, preferredPrecOffset, msdIndex, leastDigOffset,
1178 truncatedWholePart);
1179 }
1180
1181 /**
1182 * Start optional evaluation of expression and display when ready.
1183 * @param index of expression to be evaluated.
1184 * Can quietly time out without a listener callback.
Hans Boehmd4959e82016-11-15 18:01:28 -08001185 * No-op if cmi.getMaxChars() == 0.
Hans Boehm8f051c32016-10-03 16:53:58 -07001186 */
1187 public void evaluateAndNotify(long index, EvaluationListener listener, CharMetricsInfo cmi) {
Hans Boehmd4959e82016-11-15 18:01:28 -08001188 if (cmi.getMaxChars() == 0) {
1189 // Probably shouldn't happen. If it does, we didn't promise to do anything anyway.
1190 return;
1191 }
Hans Boehm31ea2522016-11-23 17:47:02 -08001192 ExprInfo ei = ensureExprIsCached(index);
Hans Boehm849d8a42016-12-12 18:19:55 -08001193 if (ei.mResultString != null && ei.mResultString != ERRONEOUS_RESULT
1194 && !(index == MAIN_INDEX && mChangedValue)) {
Hans Boehm31ea2522016-11-23 17:47:02 -08001195 // Already done. Just notify.
1196 notifyImmediately(MAIN_INDEX, mMainExpr, listener, cmi);
1197 return;
1198 } else if (ei.mEvaluator != null) {
1199 // We only allow a single listener per expression, so this request must be redundant.
1200 return;
Hans Boehm8f051c32016-10-03 16:53:58 -07001201 }
1202 evaluateResult(index, listener, cmi, false);
1203 }
1204
1205 /**
1206 * Start required evaluation of expression at given index and call back listener when ready.
Hans Boehm758cb2e2016-11-14 14:49:28 -08001207 * If index is MAIN_INDEX, we may also directly display a timeout message.
Hans Boehm8f051c32016-10-03 16:53:58 -07001208 * Uses longer timeouts than optional evaluation.
Hans Boehmd4959e82016-11-15 18:01:28 -08001209 * Requires cmi.getMaxChars() != 0.
Hans Boehm8f051c32016-10-03 16:53:58 -07001210 */
1211 public void requireResult(long index, EvaluationListener listener, CharMetricsInfo cmi) {
Hans Boehmd4959e82016-11-15 18:01:28 -08001212 if (cmi.getMaxChars() == 0) {
1213 throw new AssertionError("requireResult called too early");
1214 }
Hans Boehm8f051c32016-10-03 16:53:58 -07001215 ExprInfo ei = ensureExprIsCached(index);
1216 if (ei.mResultString == null || (index == MAIN_INDEX && mChangedValue)) {
Hans Boehmcc368502016-12-09 10:44:46 -08001217 if (index == HISTORY_MAIN_INDEX) {
1218 // We don't want to compute a result for HISTORY_MAIN_INDEX that was
1219 // not already computed for the main expression. Pretend we timed out.
Hans Boehm849d8a42016-12-12 18:19:55 -08001220 // The error case doesn't get here.
Hans Boehmcc368502016-12-09 10:44:46 -08001221 listener.onCancelled(index);
1222 } else if ((ei.mEvaluator instanceof AsyncEvaluator)
Hans Boehmd4959e82016-11-15 18:01:28 -08001223 && ((AsyncEvaluator)(ei.mEvaluator)).mRequired) {
1224 // Duplicate request; ignore.
1225 } else {
Hans Boehm83f278e2016-12-19 16:20:03 -08001226 // (Re)start evaluator in requested mode, i.e. with longer timeout.
Hans Boehmd4959e82016-11-15 18:01:28 -08001227 cancel(ei, true);
1228 evaluateResult(index, listener, cmi, true);
1229 }
Hans Boehm849d8a42016-12-12 18:19:55 -08001230 } else if (ei.mResultString == ERRONEOUS_RESULT) {
1231 // Just re-evaluate to generate a new notification.
1232 cancel(ei, true);
1233 evaluateResult(index, listener, cmi, true);
Hans Boehm8f051c32016-10-03 16:53:58 -07001234 } else {
1235 notifyImmediately(index, ei, listener, cmi);
Hans Boehm84614952014-11-25 18:46:17 -08001236 }
1237 }
1238
Hans Boehmc1ea0912015-06-19 15:05:07 -07001239 /**
Annie Chinbc001882016-11-09 19:41:21 -08001240 * Whether this expression has explicitly been evaluated (User pressed "=")
1241 */
1242 public boolean hasResult(long index) {
1243 final ExprInfo ei = ensureExprIsCached(index);
1244 return ei.mResultString != null;
1245 }
1246
1247 /**
Hans Boehm65a99a42016-02-03 18:16:07 -08001248 * Is a reevaluation still in progress?
1249 */
Hans Boehm8f051c32016-10-03 16:53:58 -07001250 public boolean evaluationInProgress(long index) {
1251 ExprInfo ei = mExprs.get(index);
1252 return ei != null && ei.mEvaluator != null;
Hans Boehm65a99a42016-02-03 18:16:07 -08001253 }
1254
1255 /**
Hans Boehmced295e2016-11-17 17:30:13 -08001256 * Cancel any current background task associated with the given ExprInfo.
Hans Boehmc1ea0912015-06-19 15:05:07 -07001257 * @param quiet suppress cancellation message
Hans Boehm8f051c32016-10-03 16:53:58 -07001258 * @return true if we cancelled an initial evaluation
Hans Boehmc1ea0912015-06-19 15:05:07 -07001259 */
Hans Boehm8f051c32016-10-03 16:53:58 -07001260 private boolean cancel(ExprInfo expr, boolean quiet) {
1261 if (expr.mEvaluator != null) {
Hans Boehm64921be2016-12-13 11:06:25 -08001262 if (quiet && (expr.mEvaluator instanceof AsyncEvaluator)) {
Annie Chine5567fd2016-12-12 13:45:24 -08001263 ((AsyncEvaluator)(expr.mEvaluator)).suppressCancelMessage();
1264 }
Hans Boehm8f051c32016-10-03 16:53:58 -07001265 // Reevaluation in progress.
Annie Chine5567fd2016-12-12 13:45:24 -08001266 if (expr.mVal.get() != null) {
Hans Boehm8f051c32016-10-03 16:53:58 -07001267 expr.mEvaluator.cancel(true);
1268 expr.mResultStringOffsetReq = expr.mResultStringOffset;
1269 // Backgound computation touches only constructive reals.
1270 // OK not to wait.
1271 expr.mEvaluator = null;
1272 } else {
Hans Boehm8f051c32016-10-03 16:53:58 -07001273 expr.mEvaluator.cancel(true);
1274 if (expr == mMainExpr) {
1275 // The expression is modifiable, and the AsyncTask is reading it.
1276 // There seems to be no good way to wait for cancellation.
1277 // Give ourselves a new copy to work on instead.
1278 mMainExpr.mExpr = (CalculatorExpr)mMainExpr.mExpr.clone();
1279 // Approximation of constructive reals should be thread-safe,
1280 // so we can let that continue until it notices the cancellation.
1281 mChangedValue = true; // Didn't do the expected evaluation.
1282 }
1283 expr.mEvaluator = null;
1284 return true;
Hans Boehmc1ea0912015-06-19 15:05:07 -07001285 }
Hans Boehm84614952014-11-25 18:46:17 -08001286 }
1287 return false;
1288 }
1289
Hans Boehmced295e2016-11-17 17:30:13 -08001290 /**
1291 * Cancel any current background task associated with the given ExprInfo.
1292 * @param quiet suppress cancellation message
1293 * @return true if we cancelled an initial evaluation
1294 */
1295 public boolean cancel(long index, boolean quiet)
1296 {
1297 ExprInfo ei = mExprs.get(index);
1298 if (ei == null) {
1299 return false;
1300 } else {
1301 return cancel(ei, quiet);
1302 }
1303 }
1304
Hans Boehm8f051c32016-10-03 16:53:58 -07001305 public void cancelAll(boolean quiet) {
1306 // TODO: May want to keep active evaluators in a HashSet to avoid traversing
1307 // all expressions we've looked at.
1308 for (ExprInfo expr: mExprs.values()) {
1309 cancel(expr, quiet);
1310 }
1311 }
1312
Hans Boehm3666e632015-07-27 18:33:12 -07001313 /**
Hans Boehm31ea2522016-11-23 17:47:02 -08001314 * Quietly cancel all evaluations associated with expressions other than the main one.
1315 * These are currently the evaluations associated with the history fragment.
1316 */
1317 public void cancelNonMain() {
1318 // TODO: May want to keep active evaluators in a HashSet to avoid traversing
1319 // all expressions we've looked at.
1320 for (ExprInfo expr: mExprs.values()) {
1321 if (expr != mMainExpr) {
1322 cancel(expr, true);
1323 }
1324 }
1325 }
1326
1327 /**
Hans Boehmced295e2016-11-17 17:30:13 -08001328 * Restore the evaluator state, including the current expression.
Hans Boehm3666e632015-07-27 18:33:12 -07001329 */
1330 public void restoreInstanceState(DataInput in) {
Hans Boehm1176f232015-05-11 16:26:03 -07001331 mChangedValue = true;
Hans Boehm84614952014-11-25 18:46:17 -08001332 try {
Hans Boehm8f051c32016-10-03 16:53:58 -07001333 mMainExpr.mDegreeMode = in.readBoolean();
1334 mMainExpr.mLongTimeout = in.readBoolean();
1335 mMainExpr.mExpr = new CalculatorExpr(in);
1336 mHasTrigFuncs = hasTrigFuncs();
Hans Boehm84614952014-11-25 18:46:17 -08001337 } catch (IOException e) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001338 Log.v("Calculator", "Exception while restoring:\n" + e);
Hans Boehm84614952014-11-25 18:46:17 -08001339 }
1340 }
1341
Hans Boehm3666e632015-07-27 18:33:12 -07001342 /**
1343 * Save the evaluator state, including the expression and any saved value.
1344 */
1345 public void saveInstanceState(DataOutput out) {
Hans Boehm84614952014-11-25 18:46:17 -08001346 try {
Hans Boehm8f051c32016-10-03 16:53:58 -07001347 out.writeBoolean(mMainExpr.mDegreeMode);
1348 out.writeBoolean(mMainExpr.mLongTimeout);
1349 mMainExpr.mExpr.write(out);
Hans Boehm84614952014-11-25 18:46:17 -08001350 } catch (IOException e) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001351 Log.v("Calculator", "Exception while saving state:\n" + e);
Hans Boehm84614952014-11-25 18:46:17 -08001352 }
1353 }
1354
Hans Boehm3666e632015-07-27 18:33:12 -07001355
1356 /**
Hans Boehm8f051c32016-10-03 16:53:58 -07001357 * Append a button press to the main expression.
Hans Boehm3666e632015-07-27 18:33:12 -07001358 * @param id Button identifier for the character or operator to be added.
1359 * @return false if we rejected the insertion due to obvious syntax issues, and the expression
1360 * is unchanged; true otherwise
1361 */
1362 public boolean append(int id) {
Hans Boehm4db31b42015-05-31 12:19:05 -07001363 if (id == R.id.fun_10pow) {
1364 add10pow(); // Handled as macro expansion.
1365 return true;
1366 } else {
Hans Boehme8553762015-06-26 17:56:52 -07001367 mChangedValue = mChangedValue || !KeyMaps.isBinary(id);
Hans Boehm8f051c32016-10-03 16:53:58 -07001368 if (mMainExpr.mExpr.add(id)) {
Hans Boehm52d477a2016-04-01 17:42:50 -07001369 if (!mHasTrigFuncs) {
1370 mHasTrigFuncs = KeyMaps.isTrigFunc(id);
1371 }
1372 return true;
1373 } else {
1374 return false;
1375 }
Hans Boehm4db31b42015-05-31 12:19:05 -07001376 }
Hans Boehm84614952014-11-25 18:46:17 -08001377 }
1378
Hans Boehm8f051c32016-10-03 16:53:58 -07001379 /**
1380 * Delete last taken from main expression.
1381 */
Hans Boehm3666e632015-07-27 18:33:12 -07001382 public void delete() {
Hans Boehmc023b732015-04-29 11:30:47 -07001383 mChangedValue = true;
Hans Boehm8f051c32016-10-03 16:53:58 -07001384 mMainExpr.mExpr.delete();
1385 if (mMainExpr.mExpr.isEmpty()) {
1386 mMainExpr.mLongTimeout = false;
Hans Boehm5e6a0ca2015-09-22 17:09:01 -07001387 }
Hans Boehm8f051c32016-10-03 16:53:58 -07001388 mHasTrigFuncs = hasTrigFuncs();
Hans Boehmc023b732015-04-29 11:30:47 -07001389 }
1390
Hans Boehm8f051c32016-10-03 16:53:58 -07001391 /**
1392 * Set degree mode for main expression.
1393 */
1394 public void setDegreeMode(boolean degreeMode) {
Hans Boehmc023b732015-04-29 11:30:47 -07001395 mChangedValue = true;
Hans Boehm8f051c32016-10-03 16:53:58 -07001396 mMainExpr.mDegreeMode = degreeMode;
Justin Klaassen8b1efdb2015-06-22 15:10:53 -07001397
1398 mSharedPrefs.edit()
1399 .putBoolean(KEY_PREF_DEGREE_MODE, degreeMode)
1400 .apply();
Hans Boehm682ff5e2015-03-09 14:40:25 -07001401 }
1402
Justin Klaassen44595162015-05-28 17:55:20 -07001403 /**
Hans Boehmced295e2016-11-17 17:30:13 -08001404 * Return an ExprInfo for a copy of the expression with the given index.
Hans Boehm8f051c32016-10-03 16:53:58 -07001405 * We remove trailing binary operators in the copy.
Hans Boehm45223152016-12-21 10:35:35 -08001406 * mTimeStamp is not copied.
Justin Klaassen44595162015-05-28 17:55:20 -07001407 */
Hans Boehm8f051c32016-10-03 16:53:58 -07001408 private ExprInfo copy(long index, boolean copyValue) {
1409 ExprInfo fromEi = mExprs.get(index);
1410 ExprInfo ei = new ExprInfo((CalculatorExpr)fromEi.mExpr.clone(), fromEi.mDegreeMode);
1411 while (ei.mExpr.hasTrailingBinary()) {
1412 ei.mExpr.delete();
1413 }
1414 if (copyValue) {
1415 ei.mVal = new AtomicReference<UnifiedReal>(fromEi.mVal.get());
1416 ei.mResultString = fromEi.mResultString;
1417 ei.mResultStringOffset = ei.mResultStringOffsetReq = fromEi.mResultStringOffset;
1418 ei.mMsdIndex = fromEi.mMsdIndex;
1419 }
1420 ei.mLongTimeout = fromEi.mLongTimeout;
1421 return ei;
Justin Klaassen44595162015-05-28 17:55:20 -07001422 }
1423
Hans Boehm3666e632015-07-27 18:33:12 -07001424 /**
Hans Boehm8f051c32016-10-03 16:53:58 -07001425 * Return an ExprInfo corresponding to the sum of the expressions at the
1426 * two indices.
Hans Boehmced295e2016-11-17 17:30:13 -08001427 * index1 should correspond to an immutable expression, and should thus NOT
1428 * be MAIN_INDEX. Index2 may be MAIN_INDEX. Both expressions are presumed
1429 * to have been evaluated. The result is unevaluated.
Hans Boehm8bf0dca2017-01-25 17:10:39 -08001430 * Can return null if evaluation resulted in an error (a very unlikely case).
Hans Boehm8f051c32016-10-03 16:53:58 -07001431 */
1432 private ExprInfo sum(long index1, long index2) {
Hans Boehm8bf0dca2017-01-25 17:10:39 -08001433 return generalized_sum(index1, index2, R.id.op_add);
Hans Boehm8f051c32016-10-03 16:53:58 -07001434 }
1435
1436 /**
Christine Franks1d99be12016-11-14 14:00:36 -08001437 * Return an ExprInfo corresponding to the subtraction of the value at the subtrahend index
1438 * from value at the minuend index (minuend - subtrahend = result). Both are presumed to have
Hans Boehm8bf0dca2017-01-25 17:10:39 -08001439 * been previously evaluated. The result is unevaluated. Can return null.
Christine Franks1d99be12016-11-14 14:00:36 -08001440 */
1441 private ExprInfo difference(long minuendIndex, long subtrahendIndex) {
Hans Boehm8bf0dca2017-01-25 17:10:39 -08001442 return generalized_sum(minuendIndex, subtrahendIndex, R.id.op_sub);
1443 }
1444
1445 private ExprInfo generalized_sum(long index1, long index2, int op) {
1446 // TODO: Consider not collapsing expr2, to save database space.
1447 // Note that this is a bit tricky, since our expressions can contain unbalanced lparens.
1448 CalculatorExpr result = new CalculatorExpr();
1449 CalculatorExpr collapsed1 = getCollapsedExpr(index1);
1450 CalculatorExpr collapsed2 = getCollapsedExpr(index2);
1451 if (collapsed1 == null || collapsed2 == null) {
1452 return null;
1453 }
1454 result.append(collapsed1);
1455 result.add(op);
1456 result.append(collapsed2);
1457 ExprInfo resultEi = new ExprInfo(result, false /* dont care about degrees/radians */);
1458 resultEi.mLongTimeout = mExprs.get(index1).mLongTimeout
1459 || mExprs.get(index2).mLongTimeout;
1460 return resultEi;
Christine Franks1d99be12016-11-14 14:00:36 -08001461 }
1462
1463 /**
Hans Boehm8f051c32016-10-03 16:53:58 -07001464 * Add the expression described by the argument to the database.
1465 * Returns the new row id in the database.
Hans Boehm45223152016-12-21 10:35:35 -08001466 * Fills in timestamp in ei, if it was not previously set.
Hans Boehm8f051c32016-10-03 16:53:58 -07001467 * If in_history is true, add it with a positive index, so it will appear in the history.
1468 */
1469 private long addToDB(boolean in_history, ExprInfo ei) {
Hans Boehmf4424772016-12-05 10:35:16 -08001470 byte[] serializedExpr = ei.mExpr.toBytes();
Hans Boehm8f051c32016-10-03 16:53:58 -07001471 ExpressionDB.RowData rd = new ExpressionDB.RowData(serializedExpr, ei.mDegreeMode,
Hans Boehm9db3ee22016-11-18 10:09:47 -08001472 ei.mLongTimeout, 0);
Hans Boehm8f051c32016-10-03 16:53:58 -07001473 long resultIndex = mExprDB.addRow(!in_history, rd);
1474 if (mExprs.get(resultIndex) != null) {
1475 throw new AssertionError("result slot already occupied! + Slot = " + resultIndex);
1476 }
1477 // Add newly assigned date to the cache.
1478 ei.mTimeStamp = rd.mTimeStamp;
Hans Boehm31ea2522016-11-23 17:47:02 -08001479 if (resultIndex == MAIN_INDEX) {
1480 throw new AssertionError("Should not store main expression");
1481 }
Hans Boehm8f051c32016-10-03 16:53:58 -07001482 mExprs.put(resultIndex, ei);
1483 return resultIndex;
1484 }
1485
1486 /**
Hans Boehm8bf0dca2017-01-25 17:10:39 -08001487 * Preserve a copy of the expression at old_index at a new index.
1488 * This is useful only of old_index is MAIN_INDEX or HISTORY_MAIN_INDEX.
Hans Boehm8f051c32016-10-03 16:53:58 -07001489 * This assumes that initial evaluation completed suceessfully.
1490 * @param in_history use a positive index so the result appears in the history.
1491 * @return the new index
1492 */
Hans Boehm8bf0dca2017-01-25 17:10:39 -08001493 public long preserve(long old_index, boolean in_history) {
1494 ExprInfo ei = copy(old_index, true);
Hans Boehm849d8a42016-12-12 18:19:55 -08001495 if (ei.mResultString == null || ei.mResultString == ERRONEOUS_RESULT) {
Hans Boehm8f051c32016-10-03 16:53:58 -07001496 throw new AssertionError("Preserving unevaluated expression");
1497 }
1498 return addToDB(in_history, ei);
1499 }
1500
1501 /**
1502 * Preserve a copy of the current main expression as the most recent history entry,
Hans Boehm3c56fad2016-11-18 11:16:57 -08001503 * assuming it is already in the database, but may have been lost from the cache.
Hans Boehm8f051c32016-10-03 16:53:58 -07001504 */
1505 public void represerve() {
Hans Boehm8f051c32016-10-03 16:53:58 -07001506 long resultIndex = getMaxIndex();
Hans Boehm45223152016-12-21 10:35:35 -08001507 // This requires database access only if the local state was preserved, but we
1508 // recreated the Evaluator. That excludes the common cases of device rotation, etc.
1509 // TODO: Revisit once we deal with database failures. We could just copy from
1510 // MAIN_INDEX instead, but that loses the timestamp.
1511 ensureExprIsCached(resultIndex);
Hans Boehm8f051c32016-10-03 16:53:58 -07001512 }
Hans Boehm3c56fad2016-11-18 11:16:57 -08001513
Hans Boehm8f051c32016-10-03 16:53:58 -07001514 /**
Hans Boehm31ea2522016-11-23 17:47:02 -08001515 * Discard previous expression in HISTORY_MAIN_INDEX and replace it by a fresh copy
1516 * of the main expression. Note that the HISTORY_MAIN_INDEX expresssion is not preserved
1517 * in the database or anywhere else; it is always reconstructed when needed.
1518 */
1519 public void copyMainToHistory() {
1520 cancel(HISTORY_MAIN_INDEX, true /* quiet */);
1521 ExprInfo ei = copy(MAIN_INDEX, true);
1522 mExprs.put(HISTORY_MAIN_INDEX, ei);
1523 }
1524
1525 /**
Hans Boehm8f051c32016-10-03 16:53:58 -07001526 * @return the {@link CalculatorExpr} representation of the result of the given
1527 * expression.
1528 * The resulting expression contains a single "token" with the pre-evaluated result.
1529 * The client should ensure that this is never invoked unless initial evaluation of the
1530 * expression has been completed.
1531 */
1532 private CalculatorExpr getCollapsedExpr(long index) {
Hans Boehm8bf0dca2017-01-25 17:10:39 -08001533 long real_index = isMutableIndex(index) ? preserve(index, false) : index;
Hans Boehm8f051c32016-10-03 16:53:58 -07001534 final ExprInfo ei = mExprs.get(real_index);
1535 final String rs = ei.mResultString;
Hans Boehm8bf0dca2017-01-25 17:10:39 -08001536 // An error can occur here only under extremely unlikely conditions.
1537 // Check anyway, and just refuse.
Annie Chin9f00a952017-02-02 13:15:33 -08001538 // rs *should* never be null, but it happens. Check as a workaround to protect against
1539 // crashes until we find the root cause (b/34801142)
1540 if (rs == ERRONEOUS_RESULT || rs == null) {
Hans Boehm8bf0dca2017-01-25 17:10:39 -08001541 return null;
1542 }
Hans Boehm8f051c32016-10-03 16:53:58 -07001543 final int dotIndex = rs.indexOf('.');
1544 final int leastDigOffset = getLsdOffset(ei.mVal.get(), rs, dotIndex);
1545 return ei.mExpr.abbreviate(real_index,
1546 getShortString(rs, getMsdIndexOf(rs), leastDigOffset));
1547 }
1548
1549 /**
1550 * Abbreviate the indicated expression to a pre-evaluated expression node,
1551 * and use that as the new main expression.
Hans Boehm3666e632015-07-27 18:33:12 -07001552 * This should not be called unless the expression was previously evaluated and produced a
1553 * non-error result. Pre-evaluated expressions can never represent an expression for which
1554 * evaluation to a constructive real diverges. Subsequent re-evaluation will also not
1555 * diverge, though it may generate errors of various kinds. E.g. sqrt(-10^-1000) .
1556 */
Hans Boehm8f051c32016-10-03 16:53:58 -07001557 public void collapse(long index) {
Hans Boehmcc368502016-12-09 10:44:46 -08001558 final boolean longTimeout = mExprs.get(index).mLongTimeout;
Hans Boehm8f051c32016-10-03 16:53:58 -07001559 final CalculatorExpr abbrvExpr = getCollapsedExpr(index);
Hans Boehmcc368502016-12-09 10:44:46 -08001560 clearMain();
Hans Boehm8f051c32016-10-03 16:53:58 -07001561 mMainExpr.mExpr.append(abbrvExpr);
Hans Boehmcc368502016-12-09 10:44:46 -08001562 mMainExpr.mLongTimeout = longTimeout;
Hans Boehm187d3e92015-06-09 18:04:26 -07001563 mChangedValue = true;
Hans Boehm52d477a2016-04-01 17:42:50 -07001564 mHasTrigFuncs = false; // Degree mode no longer affects expression value.
Hans Boehm84614952014-11-25 18:46:17 -08001565 }
1566
Hans Boehm3666e632015-07-27 18:33:12 -07001567 /**
Hans Boehmaf04c3a2016-01-27 14:50:08 -08001568 * Mark the expression as changed, preventing next evaluation request from being ignored.
1569 */
1570 public void touch() {
1571 mChangedValue = true;
1572 }
1573
Hans Boehm8f051c32016-10-03 16:53:58 -07001574 private abstract class SetWhenDoneListener implements EvaluationListener {
1575 private void badCall() {
1576 throw new AssertionError("unexpected callback");
1577 }
1578 abstract void setNow();
1579 @Override
1580 public void onCancelled(long index) {} // Extremely unlikely; leave unset.
1581 @Override
1582 public void onError(long index, int errorId) {} // Extremely unlikely; leave unset.
1583 @Override
1584 public void onEvaluate(long index, int initPrecOffset, int msdIndex, int lsdOffset,
1585 String truncatedWholePart) {
1586 setNow();
1587 }
1588 @Override
1589 public void onReevaluate(long index) {
1590 badCall();
1591 }
1592 }
1593
1594 private class SetMemoryWhenDoneListener extends SetWhenDoneListener {
1595 final long mIndex;
1596 final boolean mPersist;
1597 SetMemoryWhenDoneListener(long index, boolean persist) {
1598 mIndex = index;
1599 mPersist = persist;
1600 }
1601 @Override
1602 void setNow() {
1603 if (mMemoryIndex != 0) {
1604 throw new AssertionError("Overwriting nonzero memory index");
1605 }
1606 if (mPersist) {
1607 setMemoryIndex(mIndex);
1608 } else {
1609 mMemoryIndex = mIndex;
1610 }
1611 }
1612 }
1613
1614 private class SetSavedWhenDoneListener extends SetWhenDoneListener {
1615 final long mIndex;
1616 SetSavedWhenDoneListener(long index) {
1617 mIndex = index;
1618 }
1619 @Override
1620 void setNow() {
1621 mSavedIndex = mIndex;
1622 }
1623 }
1624
Hans Boehmaf04c3a2016-01-27 14:50:08 -08001625 /**
Hans Boehm8f051c32016-10-03 16:53:58 -07001626 * Set the local and persistent memory index.
1627 */
1628 private void setMemoryIndex(long index) {
1629 mMemoryIndex = index;
1630 mSharedPrefs.edit()
1631 .putLong(KEY_PREF_MEMORY_INDEX, index)
1632 .apply();
Christine Frankscbc51fa2017-01-04 21:00:36 -08001633
1634 if (mCallback != null) {
1635 mCallback.onMemoryStateChanged();
1636 }
Hans Boehm8f051c32016-10-03 16:53:58 -07001637 }
1638
1639 /**
1640 * Set the local and persistent saved index.
1641 */
1642 private void setSavedIndex(long index) {
1643 mSavedIndex = index;
1644 mSharedPrefs.edit()
1645 .putLong(KEY_PREF_SAVED_INDEX, index)
1646 .apply();
1647 }
1648
1649 /**
1650 * Set mMemoryIndex (possibly including the persistent version) to index when we finish
1651 * evaluating the corresponding expression.
1652 */
1653 void setMemoryIndexWhenEvaluated(long index, boolean persist) {
1654 requireResult(index, new SetMemoryWhenDoneListener(index, persist), mDummyCharMetricsInfo);
1655 }
1656
1657 /**
1658 * Set mSavedIndex (not the persistent version) to index when we finish evaluating
1659 * the corresponding expression.
1660 */
1661 void setSavedIndexWhenEvaluated(long index) {
1662 requireResult(index, new SetSavedWhenDoneListener(index), mDummyCharMetricsInfo);
1663 }
1664
1665 /**
1666 * Save an immutable version of the expression at the given index as the saved value.
Hans Boehm3666e632015-07-27 18:33:12 -07001667 * mExpr is left alone. Return false if result is unavailable.
1668 */
Hans Boehm8f051c32016-10-03 16:53:58 -07001669 private boolean copyToSaved(long index) {
Hans Boehm849d8a42016-12-12 18:19:55 -08001670 if (mExprs.get(index).mResultString == null
1671 || mExprs.get(index).mResultString == ERRONEOUS_RESULT) {
Justin Klaassen44595162015-05-28 17:55:20 -07001672 return false;
1673 }
Hans Boehm8bf0dca2017-01-25 17:10:39 -08001674 setSavedIndex(isMutableIndex(index) ? preserve(index, false) : index);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001675 return true;
1676 }
1677
Hans Boehm8f051c32016-10-03 16:53:58 -07001678 /**
1679 * Save an immutable version of the expression at the given index as the "memory" value.
1680 * The expression at index is presumed to have been evaluated.
1681 */
1682 public void copyToMemory(long index) {
Hans Boehm8bf0dca2017-01-25 17:10:39 -08001683 setMemoryIndex(isMutableIndex(index) ? preserve(index, false) : index);
Hans Boehm8f051c32016-10-03 16:53:58 -07001684 }
1685
1686 /**
1687 * Save an an expression representing the sum of "memory" and the expression with the
1688 * given index. Make mMemoryIndex point to it when we complete evaluating.
1689 */
1690 public void addToMemory(long index) {
Hans Boehmced295e2016-11-17 17:30:13 -08001691 ExprInfo newEi = sum(mMemoryIndex, index);
Hans Boehm8bf0dca2017-01-25 17:10:39 -08001692 if (newEi != null) {
1693 long newIndex = addToDB(false, newEi);
1694 mMemoryIndex = 0; // Invalidate while we're evaluating.
1695 setMemoryIndexWhenEvaluated(newIndex, true /* persist */);
1696 }
Hans Boehm8f051c32016-10-03 16:53:58 -07001697 }
1698
1699 /**
Christine Franks1d99be12016-11-14 14:00:36 -08001700 * Save an an expression representing the subtraction of the expression with the given index
1701 * from "memory." Make mMemoryIndex point to it when we complete evaluating.
1702 */
1703 public void subtractFromMemory(long index) {
1704 ExprInfo newEi = difference(mMemoryIndex, index);
Hans Boehm8bf0dca2017-01-25 17:10:39 -08001705 if (newEi != null) {
1706 long newIndex = addToDB(false, newEi);
1707 mMemoryIndex = 0; // Invalidate while we're evaluating.
1708 setMemoryIndexWhenEvaluated(newIndex, true /* persist */);
1709 }
Christine Franks1d99be12016-11-14 14:00:36 -08001710 }
1711
1712 /**
Hans Boehm8f051c32016-10-03 16:53:58 -07001713 * Return index of "saved" expression, or 0.
1714 */
1715 public long getSavedIndex() {
1716 return mSavedIndex;
1717 }
1718
1719 /**
1720 * Return index of "memory" expression, or 0.
1721 */
1722 public long getMemoryIndex() {
1723 return mMemoryIndex;
1724 }
1725
Hans Boehm3666e632015-07-27 18:33:12 -07001726 private Uri uriForSaved() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001727 return new Uri.Builder().scheme("tag")
1728 .encodedOpaquePart(mSavedName)
1729 .build();
1730 }
1731
Hans Boehm3666e632015-07-27 18:33:12 -07001732 /**
Hans Boehm8f051c32016-10-03 16:53:58 -07001733 * Save the index expression as the saved location and return a URI describing it.
1734 * The URI is used to distinguish this particular result from others we may generate.
Hans Boehm3666e632015-07-27 18:33:12 -07001735 */
Hans Boehm8f051c32016-10-03 16:53:58 -07001736 public Uri capture(long index) {
1737 if (!copyToSaved(index)) return null;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001738 // Generate a new (entirely private) URI for this result.
1739 // Attempt to conform to RFC4151, though it's unclear it matters.
Hans Boehm3666e632015-07-27 18:33:12 -07001740 final TimeZone tz = TimeZone.getDefault();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001741 DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
1742 df.setTimeZone(tz);
Hans Boehm3666e632015-07-27 18:33:12 -07001743 final String isoDate = df.format(new Date());
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001744 mSavedName = "calculator2.android.com," + isoDate + ":"
Hans Boehm3666e632015-07-27 18:33:12 -07001745 + (new Random().nextInt() & 0x3fffffff);
Hans Boehm8f051c32016-10-03 16:53:58 -07001746 mSharedPrefs.edit()
1747 .putString(KEY_PREF_SAVED_NAME, mSavedName)
1748 .apply();
Hans Boehm3666e632015-07-27 18:33:12 -07001749 return uriForSaved();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001750 }
1751
Hans Boehm3666e632015-07-27 18:33:12 -07001752 public boolean isLastSaved(Uri uri) {
Hans Boehm8f051c32016-10-03 16:53:58 -07001753 return mSavedIndex != 0 && uri.equals(uriForSaved());
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001754 }
1755
Hans Boehm3666e632015-07-27 18:33:12 -07001756 /**
Hans Boehm8f051c32016-10-03 16:53:58 -07001757 * Append the expression at index as a pre-evaluated expression to the main expression.
1758 */
1759 public void appendExpr(long index) {
Hans Boehm8bf0dca2017-01-25 17:10:39 -08001760 ExprInfo ei = mExprs.get(index);
Hans Boehm8f051c32016-10-03 16:53:58 -07001761 mChangedValue = true;
Hans Boehm8bf0dca2017-01-25 17:10:39 -08001762 mMainExpr.mLongTimeout |= ei.mLongTimeout;
1763 CalculatorExpr collapsed = getCollapsedExpr(index);
1764 if (collapsed != null) {
1765 mMainExpr.mExpr.append(getCollapsedExpr(index));
1766 }
Hans Boehm8f051c32016-10-03 16:53:58 -07001767 }
1768
1769 /**
1770 * Add the power of 10 operator to the main expression.
Hans Boehm3666e632015-07-27 18:33:12 -07001771 * This is treated essentially as a macro expansion.
1772 */
Hans Boehm4db31b42015-05-31 12:19:05 -07001773 private void add10pow() {
1774 CalculatorExpr ten = new CalculatorExpr();
1775 ten.add(R.id.digit_1);
1776 ten.add(R.id.digit_0);
1777 mChangedValue = true; // For consistency. Reevaluation is probably not useful.
Hans Boehm8f051c32016-10-03 16:53:58 -07001778 mMainExpr.mExpr.append(ten);
1779 mMainExpr.mExpr.add(R.id.op_pow);
Hans Boehm4db31b42015-05-31 12:19:05 -07001780 }
1781
Hans Boehm3666e632015-07-27 18:33:12 -07001782 /**
Hans Boehm8f051c32016-10-03 16:53:58 -07001783 * Ensure that the expression with the given index is in mExprs.
1784 * We assume that if it's either already in mExprs or mExprDB.
1785 * When we're done, the expression in mExprs may still contain references to other
1786 * subexpressions that are not yet cached.
Hans Boehm3666e632015-07-27 18:33:12 -07001787 */
Hans Boehm8f051c32016-10-03 16:53:58 -07001788 private ExprInfo ensureExprIsCached(long index) {
1789 ExprInfo ei = mExprs.get(index);
1790 if (ei != null) {
1791 return ei;
1792 }
Hans Boehm31ea2522016-11-23 17:47:02 -08001793 if (index == MAIN_INDEX) {
1794 throw new AssertionError("Main expression should be cached");
1795 }
Hans Boehm8f051c32016-10-03 16:53:58 -07001796 ExpressionDB.RowData row = mExprDB.getRow(index);
1797 DataInputStream serializedExpr =
1798 new DataInputStream(new ByteArrayInputStream(row.mExpression));
1799 try {
1800 ei = new ExprInfo(new CalculatorExpr(serializedExpr), row.degreeMode());
1801 ei.mTimeStamp = row.mTimeStamp;
Hans Boehm4452c782016-12-07 14:52:05 -08001802 ei.mLongTimeout = row.longTimeout();
Hans Boehm8f051c32016-10-03 16:53:58 -07001803 } catch(IOException e) {
1804 throw new AssertionError("IO Exception without real IO:" + e);
1805 }
1806 ExprInfo newEi = mExprs.putIfAbsent(index, ei);
1807 return newEi == null ? ei : newEi;
1808 }
1809
1810 @Override
1811 public CalculatorExpr getExpr(long index) {
1812 return ensureExprIsCached(index).mExpr;
1813 }
1814
1815 /*
1816 * Return timestamp associated with the expression in milliseconds since epoch.
1817 * Yields zero if the expression has not been written to or read from the database.
1818 */
1819 public long getTimeStamp(long index) {
1820 return ensureExprIsCached(index).mTimeStamp;
1821 }
1822
Hans Boehm8f051c32016-10-03 16:53:58 -07001823 @Override
1824 public boolean getDegreeMode(long index) {
1825 return ensureExprIsCached(index).mDegreeMode;
1826 }
1827
1828 @Override
1829 public UnifiedReal getResult(long index) {
1830 return ensureExprIsCached(index).mVal.get();
1831 }
1832
1833 @Override
1834 public UnifiedReal putResultIfAbsent(long index, UnifiedReal result) {
1835 ExprInfo ei = mExprs.get(index);
1836 if (ei.mVal.compareAndSet(null, result)) {
1837 return result;
1838 } else {
1839 // Cannot change once non-null.
1840 return ei.mVal.get();
1841 }
Hans Boehm84614952014-11-25 18:46:17 -08001842 }
1843
Hans Boehm3666e632015-07-27 18:33:12 -07001844 /**
Hans Boehm52d477a2016-04-01 17:42:50 -07001845 * Does the current main expression contain trig functions?
1846 * Might its value depend on DEG/RAD mode?
1847 */
1848 public boolean hasTrigFuncs() {
1849 return mHasTrigFuncs;
1850 }
1851
1852 /**
Hans Boehm3666e632015-07-27 18:33:12 -07001853 * Maximum number of characters in a scientific notation exponent.
1854 */
Hans Boehm0b9806f2015-06-29 16:07:15 -07001855 private static final int MAX_EXP_CHARS = 8;
1856
1857 /**
1858 * Return the index of the character after the exponent starting at s[offset].
1859 * Return offset if there is no exponent at that position.
Hans Boehm3666e632015-07-27 18:33:12 -07001860 * Exponents have syntax E[-]digit* . "E2" and "E-2" are valid. "E+2" and "e2" are not.
Hans Boehm0b9806f2015-06-29 16:07:15 -07001861 * We allow any Unicode digits, and either of the commonly used minus characters.
1862 */
Hans Boehm3666e632015-07-27 18:33:12 -07001863 public static int exponentEnd(String s, int offset) {
Hans Boehm0b9806f2015-06-29 16:07:15 -07001864 int i = offset;
1865 int len = s.length();
1866 if (i >= len - 1 || s.charAt(i) != 'E') {
1867 return offset;
1868 }
1869 ++i;
1870 if (KeyMaps.keyForChar(s.charAt(i)) == R.id.op_sub) {
1871 ++i;
1872 }
Hans Boehm79302b02015-08-05 17:20:32 -07001873 if (i == len || !Character.isDigit(s.charAt(i))) {
Hans Boehm0b9806f2015-06-29 16:07:15 -07001874 return offset;
1875 }
1876 ++i;
1877 while (i < len && Character.isDigit(s.charAt(i))) {
1878 ++i;
Hans Boehm79302b02015-08-05 17:20:32 -07001879 if (i > offset + MAX_EXP_CHARS) {
1880 return offset;
1881 }
Hans Boehm0b9806f2015-06-29 16:07:15 -07001882 }
1883 return i;
1884 }
1885
1886 /**
1887 * Add the exponent represented by s[begin..end) to the constant at the end of current
1888 * expression.
Hans Boehm3666e632015-07-27 18:33:12 -07001889 * The end of the current expression must be a constant. Exponents have the same syntax as
1890 * for exponentEnd().
Hans Boehm0b9806f2015-06-29 16:07:15 -07001891 */
Hans Boehm3666e632015-07-27 18:33:12 -07001892 public void addExponent(String s, int begin, int end) {
Hans Boehm0b9806f2015-06-29 16:07:15 -07001893 int sign = 1;
1894 int exp = 0;
1895 int i = begin + 1;
1896 // We do the decimal conversion ourselves to exactly match exponentEnd() conventions
1897 // and handle various kinds of digits on input. Also avoids allocation.
1898 if (KeyMaps.keyForChar(s.charAt(i)) == R.id.op_sub) {
1899 sign = -1;
1900 ++i;
1901 }
1902 for (; i < end; ++i) {
1903 exp = 10 * exp + Character.digit(s.charAt(i), 10);
1904 }
Hans Boehm8f051c32016-10-03 16:53:58 -07001905 mMainExpr.mExpr.addExponent(sign * exp);
Hans Boehm0b9806f2015-06-29 16:07:15 -07001906 mChangedValue = true;
1907 }
Hans Boehm8f051c32016-10-03 16:53:58 -07001908
1909 /**
1910 * Generate a String representation of the expression at the given index.
1911 * This has the side effect of adding the expression to mExprs.
1912 * The expression must exist in the database.
1913 */
1914 public String getExprAsString(long index) {
Hans Boehm3c56fad2016-11-18 11:16:57 -08001915 return getExprAsSpannable(index).toString();
Hans Boehm8f051c32016-10-03 16:53:58 -07001916 }
1917
Annie Chin06fd3cf2016-11-07 16:04:33 -08001918 public Spannable getExprAsSpannable(long index) {
Christine Frankscbc51fa2017-01-04 21:00:36 -08001919 return getExpr(index).toSpannableStringBuilder(mContext);
Annie Chin06fd3cf2016-11-07 16:04:33 -08001920 }
1921
Hans Boehm8f051c32016-10-03 16:53:58 -07001922 /**
1923 * Generate a String representation of all expressions in the database.
1924 * Debugging only.
1925 */
1926 public String historyAsString() {
1927 final long startIndex = getMinIndex();
1928 final long endIndex = getMaxIndex();
1929 final StringBuilder sb = new StringBuilder();
1930 for (long i = getMinIndex(); i < ExpressionDB.MAXIMUM_MIN_INDEX; ++i) {
1931 sb.append(i).append(": ").append(getExprAsString(i)).append("\n");
1932 }
1933 for (long i = 1; i < getMaxIndex(); ++i) {
1934 sb.append(i).append(": ").append(getExprAsString(i)).append("\n");
1935 }
1936 sb.append("Memory index = ").append(getMemoryIndex());
1937 sb.append(" Saved index = ").append(getSavedIndex()).append("\n");
1938 return sb.toString();
1939 }
Hans Boehma5ea8eb2016-12-01 12:33:38 -08001940
1941 /**
Hans Boehme95203e2017-01-04 14:13:11 -08001942 * Wait for pending writes to the database to complete.
1943 */
1944 public void waitForWrites() {
1945 mExprDB.waitForWrites();
1946 }
1947
1948 /**
Hans Boehma5ea8eb2016-12-01 12:33:38 -08001949 * Destroy the current evaluator, forcing getEvaluator to allocate a new one.
1950 * This is needed for testing, since Robolectric apparently doesn't let us preserve
1951 * an open databse across tests. Cf. https://github.com/robolectric/robolectric/issues/1890 .
1952 */
1953 public void destroyEvaluator() {
1954 mExprDB.close();
1955 evaluator = null;
1956 }
Christine Frankscbc51fa2017-01-04 21:00:36 -08001957
1958 public interface Callback {
1959 void onMemoryStateChanged();
1960 void showMessageDialog(@StringRes int title, @StringRes int message,
1961 @StringRes int positiveButtonLabel, String tag);
1962 }
Hans Boehm84614952014-11-25 18:46:17 -08001963}