1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.calculator2; 18 19 import android.content.Context; 20 import android.content.SharedPreferences; 21 import android.net.Uri; 22 import android.os.AsyncTask; 23 import android.os.Handler; 24 import android.preference.PreferenceManager; 25 import android.support.annotation.NonNull; 26 import android.support.annotation.StringRes; 27 import android.support.annotation.VisibleForTesting; 28 import android.text.Spannable; 29 import android.util.Log; 30 31 import com.hp.creals.CR; 32 33 import java.io.ByteArrayInputStream; 34 import java.io.DataInput; 35 import java.io.DataInputStream; 36 import java.io.DataOutput; 37 import java.io.IOException; 38 import java.text.DateFormat; 39 import java.text.SimpleDateFormat; 40 import java.util.Date; 41 import java.util.Random; 42 import java.util.TimeZone; 43 import java.util.concurrent.ConcurrentHashMap; 44 import java.util.concurrent.atomic.AtomicReference; 45 46 /** 47 * 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 57 * precisions. 58 * 59 * Actual expressions being evaluated are represented as {@link CalculatorExpr}s. 60 * 61 * 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 64 * in-progress computations by invoking the cancelAll() method. 65 * 66 * When evaluation is requested, we invoke the eval() method on the CalculatorExpr from a 67 * 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 72 * 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 77 * 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. 81 * 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 */ 98 public class Evaluator implements CalculatorExpr.ExprResolver { 99 100 private static Evaluator evaluator; 101 102 public static String TIMEOUT_DIALOG_TAG = "timeout"; 103 104 @NonNull getInstance(Context context)105 public static Evaluator getInstance(Context context) { 106 if (evaluator == null) { 107 evaluator = new Evaluator(context.getApplicationContext()); 108 } 109 return evaluator; 110 } 111 112 public interface EvaluationListener { 113 /** 114 * Called if evaluation was explicitly cancelled or evaluation timed out. 115 */ onCancelled(long index)116 public void onCancelled(long index); 117 /** 118 * Called if evaluation resulted in an error. 119 */ onError(long index, int errorId)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 */ onEvaluate(long index, int initPrecOffset, int msdIndex, int lsdOffset, String truncatedWholePart)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 */ onReevaluate(long index)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 */ getMaxChars()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. 157 * Returns zero if metrics information is not yet available. 158 */ separatorChars(String s, int len)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 */ getDecimalCredit()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 */ getNoEllipsisCredit()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 getMaxChars()178 public int getMaxChars() { 179 return SHORT_TARGET_LENGTH + 10; 180 } 181 @Override separatorChars(String s, int len)182 public float separatorChars(String s, int len) { 183 return 0; 184 } 185 @Override getDecimalCredit()186 public float getDecimalCredit() { 187 return 0; 188 } 189 @Override getNoEllipsisCredit()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 199 // a partial result, we copy the main expression to a non-zero index. 200 // 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. 203 // 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. 207 // 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. 211 212 // 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 216 // 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.) 219 220 private static final String KEY_PREF_DEGREE_MODE = "degree_mode"; 221 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"; 224 225 // 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. 228 private static final int EXTRA_DIGITS = 20; 229 230 // 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. 246 private static final int INIT_PREC = 50; 247 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 250 // 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 252 // we always can if we have a rational representation. 253 private static final int MAX_MSD_PREC_OFFSET = 1100; 254 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. 257 private static final int EXP_COST = 3; 258 259 // 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; 264 265 // A hopefully unique name associated with mSaved. 266 private String mSavedName; 267 268 // The main expression may have changed since the last evaluation in ways that would affect its 269 // value. 270 private boolean mChangedValue; 271 272 // The main expression contains trig functions. 273 private boolean mHasTrigFuncs; 274 275 public static final int INVALID_MSD = Integer.MAX_VALUE; 276 277 // Used to represent an erroneous result or a required evaluation. Not displayed. 278 private static final String ERRONEOUS_RESULT = "ERR"; 279 280 /** 281 * An individual CalculatorExpr, together with its evaluation state. 282 * 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. 284 * 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. ExprInfo(CalculatorExpr expr, boolean dm)295 public ExprInfo(CalculatorExpr expr, boolean dm) { 296 mExpr = expr; 297 mDegreeMode = dm; 298 mVal = new AtomicReference<UnifiedReal>(); 299 } 300 301 // Currently running expression evaluator, if any. This is either an AsyncEvaluator 302 // (if mResultString == null or it's obsolete), or an AsyncReevaluator. 303 // 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. 306 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. 314 // ERRONEOUS_RESULT indicates evaluation resulted in an error. 315 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; 326 } 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; 336 337 private final Handler mTimeoutHandler; // Used to schedule evaluation timeouts. 338 setMainExpr(ExprInfo expr)339 private void setMainExpr(ExprInfo expr) { 340 mMainExpr = expr; 341 mExprs.put(MAIN_INDEX, expr); 342 } 343 Evaluator(Context context)344 Evaluator(Context context) { 345 mContext = context; 346 setMainExpr(new ExprInfo(new CalculatorExpr(), false)); 347 mSavedName = "none"; 348 mTimeoutHandler = new Handler(); 349 350 mExprDB = new ExpressionDB(context); 351 mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); 352 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); 355 if (savedIndex != 0 && savedIndex != -1 /* Recover from old corruption */) { 356 setSavedIndexWhenEvaluated(savedIndex); 357 } 358 if (memoryIndex != 0 && memoryIndex != -1) { 359 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 */ getMinIndex()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 */ getMaxIndex()382 public long getMaxIndex() { 383 return mExprDB.getMaxIndex(); 384 } 385 386 /** 387 * Set the Callback for showing dialogs and notifying the UI about memory state changes. 388 * @param callback 389 */ setCallback(Callback callback)390 public void setCallback(Callback callback) { 391 mCallback = callback; 392 } 393 394 /** 395 * Does the expression index refer to a transient and mutable expression? 396 */ isMutableIndex(long index)397 private boolean isMutableIndex(long index) { 398 return index == MAIN_INDEX || index == HISTORY_MAIN_INDEX; 399 } 400 401 /** 402 * Result of initial asynchronous result computation. 403 * Represents either an error or a result computed to an initial evaluation precision. 404 */ 405 private static class InitialResult { 406 public final int errorResourceId; // Error string or INVALID_RES_ID. 407 public final UnifiedReal val; // Constructive real value. 408 public final String newResultString; // Null iff it can't be computed. 409 public final int newResultStringOffset; 410 public final int initDisplayOffset; InitialResult(UnifiedReal v, String s, int p, int idp)411 InitialResult(UnifiedReal v, String s, int p, int idp) { 412 errorResourceId = Calculator.INVALID_RES_ID; 413 val = v; 414 newResultString = s; 415 newResultStringOffset = p; 416 initDisplayOffset = idp; 417 } InitialResult(int errorId)418 InitialResult(int errorId) { 419 errorResourceId = errorId; 420 val = UnifiedReal.ZERO; 421 newResultString = "BAD"; 422 newResultStringOffset = 0; 423 initDisplayOffset = 0; 424 } isError()425 boolean isError() { 426 return errorResourceId != Calculator.INVALID_RES_ID; 427 } 428 } 429 displayCancelledMessage()430 private void displayCancelledMessage() { 431 if (mCallback != null) { 432 mCallback.showMessageDialog(0, R.string.cancelled, 0, null); 433 } 434 } 435 436 // 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. 441 442 /** 443 * Return the timeout in milliseconds. 444 * @param longTimeout a long timeout is in effect 445 */ getTimeout(boolean longTimeout)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. 450 } 451 452 /** 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 */ getMaxResultBits(boolean longTimeout)456 private int getMaxResultBits(boolean longTimeout) { 457 return longTimeout ? 700000 : 240000; 458 } 459 460 /** 461 * Timeout for unrequested, speculative evaluations, in milliseconds. 462 */ 463 private static final long QUICK_TIMEOUT = 1000; 464 465 /** 466 * 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 /** 476 * Maximum result bit length for unrequested, speculative evaluations. 477 * Also used to bound evaluation precision for small non-zero fractions. 478 */ 479 private static final int QUICK_MAX_RESULT_BITS = 150000; 480 displayTimeoutMessage(boolean longTimeout)481 private void displayTimeoutMessage(boolean longTimeout) { 482 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 } 486 } 487 setLongTimeout()488 public void setLongTimeout() { 489 mMainExpr.mLongTimeout = true; 490 } 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 */ 498 class AsyncEvaluator extends AsyncTask<Void, Void, InitialResult> { 499 private boolean mDm; // degrees 500 public boolean mRequired; // Result was requested by user. 501 private boolean mQuiet; // Suppress cancellation message. 502 private Runnable mTimeoutRunnable = null; 503 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 AsyncEvaluator(long index, EvaluationListener listener, CharMetricsInfo cmi, boolean dm, boolean required)508 AsyncEvaluator(long index, EvaluationListener listener, CharMetricsInfo cmi, boolean dm, 509 boolean required) { 510 mIndex = index; 511 mListener = listener; 512 mCharMetricsInfo = cmi; 513 mDm = dm; 514 mRequired = required; 515 mQuiet = !required || mIndex != MAIN_INDEX; 516 mExprInfo = mExprs.get(mIndex); 517 if (mExprInfo.mEvaluator != null) { 518 throw new AssertionError("Evaluation already in progress!"); 519 } 520 } 521 handleTimeout()522 private void handleTimeout() { 523 // Runs in UI thread. 524 boolean running = (getStatus() != AsyncTask.Status.FINISHED); 525 if (running && cancel(true)) { 526 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(); 530 suppressCancelMessage(); 531 displayTimeoutMessage(mExprInfo.mLongTimeout); 532 } 533 } 534 } 535 suppressCancelMessage()536 private void suppressCancelMessage() { 537 mQuiet = true; 538 } 539 540 @Override onPreExecute()541 protected void onPreExecute() { 542 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. 548 timeout = NON_MAIN_TIMEOUT; 549 } 550 mTimeoutRunnable = new Runnable() { 551 @Override 552 public void run() { 553 handleTimeout(); 554 } 555 }; 556 mTimeoutHandler.removeCallbacks(mTimeoutRunnable); 557 mTimeoutHandler.postDelayed(mTimeoutRunnable, timeout); 558 } 559 560 /** 561 * Is a computed result too big for decimal conversion? 562 */ isTooBig(UnifiedReal res)563 private boolean isTooBig(UnifiedReal res) { 564 final int maxBits = mRequired ? getMaxResultBits(mExprInfo.mLongTimeout) 565 : QUICK_MAX_RESULT_BITS; 566 return res.approxWholeNumberBitsGreaterThan(maxBits); 567 } 568 569 @Override doInBackground(Void... nothing)570 protected InitialResult doInBackground(Void... nothing) { 571 try { 572 // 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) { 575 try { 576 res = mExprInfo.mExpr.eval(mDm, Evaluator.this); 577 if (isCancelled()) { 578 // TODO: This remains very slightly racey. Fix this. 579 throw new CR.AbortedException(); 580 } 581 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 } 587 } 588 if (isTooBig(res)) { 589 // Avoid starting a long uninterruptible decimal conversion. 590 return new InitialResult(R.string.timeout); 591 } 592 int precOffset = INIT_PREC; 593 String initResult = res.toStringTruncated(precOffset); 594 int msd = getMsdIndexOf(initResult); 595 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 } 612 } 613 final int lsdOffset = getLsdOffset(res, initResult, initResult.indexOf('.')); 614 final int initDisplayOffset = getPreferredPrec(initResult, msd, lsdOffset, 615 mCharMetricsInfo); 616 final int newPrecOffset = initDisplayOffset + EXTRA_DIGITS; 617 if (newPrecOffset > precOffset) { 618 precOffset = newPrecOffset; 619 initResult = res.toStringTruncated(precOffset); 620 } 621 return new InitialResult(res, initResult, precOffset, initDisplayOffset); 622 } catch (CalculatorExpr.SyntaxException e) { 623 return new InitialResult(R.string.error_syntax); 624 } catch (UnifiedReal.ZeroDivisionException e) { 625 return new InitialResult(R.string.error_zero_divide); 626 } catch(ArithmeticException e) { 627 return new InitialResult(R.string.error_nan); 628 } catch(CR.PrecisionOverflowException e) { 629 // Extremely unlikely unless we're actually dividing by zero or the like. 630 return new InitialResult(R.string.error_overflow); 631 } catch(CR.AbortedException e) { 632 return new InitialResult(R.string.error_aborted); 633 } 634 } 635 636 @Override onPostExecute(InitialResult result)637 protected void onPostExecute(InitialResult result) { 638 mExprInfo.mEvaluator = null; 639 mTimeoutHandler.removeCallbacks(mTimeoutRunnable); 640 if (result.isError()) { 641 if (result.errorResourceId == R.string.timeout) { 642 // Emulating timeout due to large result. 643 if (mRequired && mIndex == MAIN_INDEX) { 644 displayTimeoutMessage(mExprs.get(mIndex).mLongTimeout); 645 } 646 mListener.onCancelled(mIndex); 647 } else { 648 if (mRequired) { 649 mExprInfo.mResultString = ERRONEOUS_RESULT; 650 } 651 mListener.onError(mIndex, result.errorResourceId); 652 } 653 return; 654 } 655 // mExprInfo.mVal was already set asynchronously by child thread. 656 mExprInfo.mResultString = result.newResultString; 657 mExprInfo.mResultStringOffset = result.newResultStringOffset; 658 final int dotIndex = mExprInfo.mResultString.indexOf('.'); 659 String truncatedWholePart = mExprInfo.mResultString.substring(0, dotIndex); 660 // 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 662 // been conservative. 663 // TODO: Could optimize by remembering display size and checking for change. 664 int initPrecOffset = result.initDisplayOffset; 665 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); 670 if (newInitPrecOffset < initPrecOffset) { 671 initPrecOffset = newInitPrecOffset; 672 } else { 673 // They should be equal. But nothing horrible should happen if they're not. e.g. 674 // because CalculatorResult.MAX_WIDTH was too small. 675 } 676 mListener.onEvaluate(mIndex, initPrecOffset, mExprInfo.mMsdIndex, leastDigOffset, 677 truncatedWholePart); 678 } 679 680 @Override onCancelled(InitialResult result)681 protected void onCancelled(InitialResult result) { 682 // Invoker resets mEvaluator. 683 mTimeoutHandler.removeCallbacks(mTimeoutRunnable); 684 if (!mQuiet) { 685 displayCancelledMessage(); 686 } // Otherwise, if mRequired, timeout processing displayed message. 687 mListener.onCancelled(mIndex); 688 // Just drop the evaluation; Leave expression displayed. 689 return; 690 } 691 } 692 693 /** 694 * 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 unflipZeroes(String oldDigs, int oldPrecOffset, String newDigs, int newPrecOffset)705 public static String unflipZeroes(String oldDigs, int oldPrecOffset, String newDigs, 706 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. 719 if (!newDigs.substring(newLen - precDiff).equals(StringUtils.repeat('0', precDiff))) { 720 throw new AssertionError("New approximation invalidates old one!"); 721 } 722 return oldDigs + StringUtils.repeat('9', precDiff); 723 } 724 725 /** 726 * Result of asynchronous reevaluation. 727 */ 728 private static class ReevalResult { 729 public final String newResultString; 730 public final int newResultStringOffset; ReevalResult(String s, int p)731 ReevalResult(String s, int p) { 732 newResultString = s; 733 newResultStringOffset = p; 734 } 735 } 736 737 /** 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. 741 * This assumes that initial evaluation of the expression has been successfully 742 * completed. 743 */ 744 private class AsyncReevaluator extends AsyncTask<Integer, Void, ReevalResult> { 745 private long mIndex; // Index of expression to evaluate. 746 private EvaluationListener mListener; 747 private ExprInfo mExprInfo; 748 AsyncReevaluator(long index, EvaluationListener listener)749 AsyncReevaluator(long index, EvaluationListener listener) { 750 mIndex = index; 751 mListener = listener; 752 mExprInfo = mExprs.get(mIndex); 753 } 754 755 @Override doInBackground(Integer... prec)756 protected ReevalResult doInBackground(Integer... prec) { 757 try { 758 final int precOffset = prec[0].intValue(); 759 return new ReevalResult(mExprInfo.mVal.get().toStringTruncated(precOffset), 760 precOffset); 761 } 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 } 771 772 @Override onPostExecute(ReevalResult result)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. 778 mExprInfo.mResultString = ERRONEOUS_RESULT; 779 mListener.onError(mIndex, R.string.error_nan); 780 } else { 781 if (result.newResultStringOffset < mExprInfo.mResultStringOffset) { 782 throw new AssertionError("Unexpected onPostExecute timing"); 783 } 784 mExprInfo.mResultString = unflipZeroes(mExprInfo.mResultString, 785 mExprInfo.mResultStringOffset, result.newResultString, 786 result.newResultStringOffset); 787 mExprInfo.mResultStringOffset = result.newResultStringOffset; 788 mListener.onReevaluate(mIndex); 789 } 790 mExprInfo.mEvaluator = null; 791 } 792 // On cancellation we do nothing; invoker should have left no trace of us. 793 } 794 795 /** 796 * 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. 798 * Only called if prior evaluation succeeded. 799 */ ensureCachePrec(long index, int precOffset, EvaluationListener listener)800 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) { 805 // Ensure we only have one evaluation running at a time. 806 ei.mEvaluator.cancel(true); 807 ei.mEvaluator = null; 808 } 809 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; 814 } 815 reEval.execute(ei.mResultStringOffsetReq); 816 } 817 818 /** 819 * Return the rightmost nonzero digit position, if any. 820 * @param val UnifiedReal value of result. 821 * @param cache Current cached decimal string representation of result. 822 * @param decIndex Index of decimal point in cache. 823 * @result Position of rightmost nonzero digit relative to decimal point. 824 * Integer.MIN_VALUE if we cannot determine. Integer.MAX_VALUE if there is no lsd, 825 * or we cannot determine it. 826 */ getLsdOffset(UnifiedReal val, String cache, int decIndex)827 static int getLsdOffset(UnifiedReal val, String cache, int decIndex) { 828 if (val.definitelyZero()) return Integer.MIN_VALUE; 829 int result = val.digitsRequired(); 830 if (result == 0) { 831 int i; 832 for (i = -1; decIndex + i > 0 && cache.charAt(decIndex + i) == '0'; --i) { } 833 result = i; 834 } 835 return result; 836 } 837 838 // 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 842 /** 843 * Retrieve the preferred precision "offset" for the currently displayed result. 844 * 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. 848 * @param lastDigitOffset Position of least significant digit (1 = tenths digit) 849 * or Integer.MAX_VALUE. 850 */ getPreferredPrec(String cache, int msd, int lastDigitOffset, CharMetricsInfo cm)851 private static int getPreferredPrec(String cache, int msd, int lastDigitOffset, 852 CharMetricsInfo cm) { 853 final int lineLength = cm.getMaxChars(); 854 final int wholeSize = cache.indexOf('.'); 855 final float rawSepChars = cm.separatorChars(cache, wholeSize); 856 final float rawSepCharsNoDecimal = rawSepChars - cm.getNoEllipsisCredit(); 857 final float rawSepCharsWithDecimal = rawSepCharsNoDecimal - cm.getDecimalCredit(); 858 final int sepCharsNoDecimal = (int) Math.ceil(Math.max(rawSepCharsNoDecimal, 0.0f)); 859 final int sepCharsWithDecimal = (int) Math.ceil(Math.max(rawSepCharsWithDecimal, 0.0f)); 860 final int negative = cache.charAt(0) == '-' ? 1 : 0; 861 // Don't display decimal point if result is an integer. 862 if (lastDigitOffset == 0) { 863 lastDigitOffset = -1; 864 } 865 if (lastDigitOffset != Integer.MAX_VALUE) { 866 if (wholeSize <= lineLength - sepCharsNoDecimal && lastDigitOffset <= 0) { 867 // Exact integer. Prefer to display as integer, without decimal point. 868 return -1; 869 } 870 if (lastDigitOffset >= 0 871 && wholeSize + lastDigitOffset + 1 /* decimal pt. */ 872 <= lineLength - sepCharsWithDecimal) { 873 // Display full exact number without scientific notation. 874 return lastDigitOffset; 875 } 876 } 877 if (msd > wholeSize && msd <= wholeSize + EXP_COST + 1) { 878 // Display number without scientific notation. Treat leading zero as msd. 879 msd = wholeSize - 1; 880 } 881 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. 886 return lineLength - 2; 887 } 888 // 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; 902 } 903 904 private static final int SHORT_TARGET_LENGTH = 8; 905 private static final String SHORT_UNCERTAIN_ZERO = "0.00000" + KeyMaps.ELLIPSIS; 906 907 /** 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. 916 * @param lsdOffset Position of least significant digit in finite representation, 917 * relative to decimal point, or MAX_VALUE. 918 */ getShortString(String cache, int msdIndex, int lsdOffset)919 private static String getShortString(String cache, int msdIndex, int lsdOffset) { 920 // 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) { 934 if (lsdOffset < INIT_PREC) { 935 return "0"; 936 } else { 937 return SHORT_UNCERTAIN_ZERO; 938 } 939 } 940 // Avoid scientific notation for small numbers of zeros. 941 // Instead stretch significant digits to include decimal point. 942 if (lsdOffset < -1 && dotIndex - msdIndex + negative <= SHORT_TARGET_LENGTH 943 && lsdOffset >= -CalculatorResult.MAX_TRAILING_ZEROES - 1) { 944 // Whole number that fits in allotted space. 945 // CalculatorResult would not use scientific notation either. 946 lsdOffset = -1; 947 } 948 if (msdIndex > dotIndex) { 949 if (msdIndex <= dotIndex + EXP_COST + 1) { 950 // Preferred display format in this case is with leading zeroes, even if 951 // it doesn't fit entirely. Replicate that here. 952 msdIndex = dotIndex - 1; 953 } else if (lsdOffset <= SHORT_TARGET_LENGTH - negative - 2 954 && lsdOffset <= CalculatorResult.MAX_LEADING_ZEROES + 1) { 955 // 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 } 965 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) { 969 // Fits, no exponent needed. 970 final String wholeWithCommas = StringUtils.addCommas(cache, msdIndex, dotIndex); 971 return negativeSign + wholeWithCommas + cache.substring(dotIndex, lsdIndex + 1); 972 } 973 if (totalDigits <= SHORT_TARGET_LENGTH - 3) { 974 return negativeSign + cache.charAt(msdIndex) + "." 975 + cache.substring(msdIndex + 1, lsdIndex + 1) + "E" + exponent; 976 } 977 } 978 // We need to abbreviate. 979 if (dotIndex > msdIndex && dotIndex < msdIndex + SHORT_TARGET_LENGTH - negative - 1) { 980 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; 984 } 985 // Need abbreviation + exponent 986 return negativeSign + cache.charAt(msdIndex) + "." 987 + cache.substring(msdIndex + 1, msdIndex + SHORT_TARGET_LENGTH - negative - 4) 988 + KeyMaps.ELLIPSIS + "E" + exponent; 989 } 990 991 /** 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 */ getMsdIndexOf(String s)996 public static int getMsdIndexOf(String s) { 997 final int len = s.length(); 998 int nonzeroIndex = -1; 999 for (int i = 0; i < len; ++i) { 1000 char c = s.charAt(i); 1001 if (c != '-' && c != '.' && c != '0') { 1002 nonzeroIndex = i; 1003 break; 1004 } 1005 } 1006 if (nonzeroIndex >= 0 && (nonzeroIndex < len - 1 || s.charAt(nonzeroIndex) != '1')) { 1007 return nonzeroIndex; 1008 } else { 1009 return INVALID_MSD; 1010 } 1011 } 1012 1013 /** 1014 * Return most significant digit index for the result of the expressin at the given index. 1015 * 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. 1017 * Result is almost consistent through reevaluations: It may increase by one, once. 1018 */ getMsdIndex(long index)1019 private int getMsdIndex(long index) { 1020 ExprInfo ei = mExprs.get(index); 1021 if (ei.mMsdIndex != INVALID_MSD) { 1022 // 0.100000... can change to 0.0999999... We may have to correct once by one digit. 1023 if (ei.mResultString.charAt(ei.mMsdIndex) == '0') { 1024 ei.mMsdIndex++; 1025 } 1026 return ei.mMsdIndex; 1027 } 1028 if (ei.mVal.get().definitelyZero()) { 1029 return INVALID_MSD; // None exists 1030 } 1031 int result = INVALID_MSD; 1032 if (ei.mResultString != null) { 1033 result = ei.mMsdIndex = getMsdIndexOf(ei.mResultString); 1034 } 1035 return result; 1036 } 1037 1038 // 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 1046 * cache contents, but it may contain blanks for unknown digits. It may also use 1047 * 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. 1054 * 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. 1056 * 1057 * @param index Index of expression to approximate 1058 * @param precOffset Zeroth element indicates desired and actual precision 1059 * @param maxPrecOffset Maximum adjusted precOffset[0] 1060 * @param maxDigs Maximum length of result 1061 * @param truncated Zeroth element is set if leading nonzero digits were dropped 1062 * @param negative Zeroth element is set of the result is negative. 1063 * @param listener EvaluationListener to notify when reevaluation is complete. 1064 */ getString(long index, int[] precOffset, int maxPrecOffset, int maxDigs, boolean[] truncated, boolean[] negative, EvaluationListener listener)1065 public String getString(long index, int[] precOffset, int maxPrecOffset, int maxDigs, 1066 boolean[] truncated, boolean[] negative, EvaluationListener listener) { 1067 ExprInfo ei = mExprs.get(index); 1068 int currentPrecOffset = precOffset[0]; 1069 // Make sure we eventually get a complete answer 1070 if (ei.mResultString == null) { 1071 ensureCachePrec(index, currentPrecOffset + EXTRA_DIGITS, listener); 1072 // Nothing else to do now; seems to happen on rare occasion with weird user input 1073 // timing; Will repair itself in a jiffy. 1074 return " "; 1075 } else { 1076 ensureCachePrec(index, currentPrecOffset + EXTRA_DIGITS + ei.mResultString.length() 1077 / EXTRA_DIVISOR, listener); 1078 } 1079 // Compute an appropriate substring of mResultString. Pad if necessary. 1080 final int len = ei.mResultString.length(); 1081 final boolean myNegative = ei.mResultString.charAt(0) == '-'; 1082 negative[0] = myNegative; 1083 // Don't scroll left past leftmost digits in mResultString unless that still leaves an 1084 // integer. 1085 int integralDigits = len - ei.mResultStringOffset; 1086 // includes 1 for dec. pt 1087 if (myNegative) { 1088 --integralDigits; 1089 } 1090 int minPrecOffset = Math.min(MIN_DISPLAYED_DIGS - integralDigits, -1); 1091 currentPrecOffset = Math.min(Math.max(currentPrecOffset, minPrecOffset), 1092 maxPrecOffset); 1093 precOffset[0] = currentPrecOffset; 1094 int extraDigs = ei.mResultStringOffset - currentPrecOffset; // trailing digits to drop 1095 int deficit = 0; // The number of digits we're short 1096 if (extraDigs < 0) { 1097 extraDigs = 0; 1098 deficit = Math.min(currentPrecOffset - ei.mResultStringOffset, maxDigs); 1099 } 1100 int endIndex = len - extraDigs; 1101 if (endIndex < 1) { 1102 return " "; 1103 } 1104 int startIndex = Math.max(endIndex + deficit - maxDigs, 0); 1105 truncated[0] = (startIndex > getMsdIndex(index)); 1106 String result = ei.mResultString.substring(startIndex, endIndex); 1107 if (deficit > 0) { 1108 result += StringUtils.repeat(' ', deficit); 1109 // Blank character is replaced during translation. 1110 // 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; 1115 } 1116 1117 /** 1118 * Clear the cache for the main expression. 1119 */ clearMainCache()1120 private void clearMainCache() { 1121 mMainExpr.mVal.set(null); 1122 mMainExpr.mResultString = null; 1123 mMainExpr.mResultStringOffset = mMainExpr.mResultStringOffsetReq = 0; 1124 mMainExpr.mMsdIndex = INVALID_MSD; 1125 } 1126 1127 clearMain()1128 public void clearMain() { 1129 mMainExpr.mExpr.clear(); 1130 mHasTrigFuncs = false; 1131 clearMainCache(); 1132 mMainExpr.mLongTimeout = false; 1133 } 1134 clearEverything()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)); 1143 } 1144 1145 /** 1146 * 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. 1150 * @param required result was explicitly requested by user. 1151 */ evaluateResult(long index, EvaluationListener listener, CharMetricsInfo cmi, boolean required)1152 private void evaluateResult(long index, EvaluationListener listener, CharMetricsInfo cmi, 1153 boolean required) { 1154 ExprInfo ei = mExprs.get(index); 1155 if (index == MAIN_INDEX) { 1156 clearMainCache(); 1157 } // Otherwise the expression is immutable. 1158 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; 1163 } 1164 } 1165 1166 /** 1167 * Notify listener of a previously completed evaluation. 1168 */ notifyImmediately(long index, ExprInfo ei, EvaluationListener listener, CharMetricsInfo cmi)1169 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. 1185 * No-op if cmi.getMaxChars() == 0. 1186 */ evaluateAndNotify(long index, EvaluationListener listener, CharMetricsInfo cmi)1187 public void evaluateAndNotify(long index, EvaluationListener listener, CharMetricsInfo cmi) { 1188 if (cmi.getMaxChars() == 0) { 1189 // Probably shouldn't happen. If it does, we didn't promise to do anything anyway. 1190 return; 1191 } 1192 ExprInfo ei = ensureExprIsCached(index); 1193 if (ei.mResultString != null && ei.mResultString != ERRONEOUS_RESULT 1194 && !(index == MAIN_INDEX && mChangedValue)) { 1195 // 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; 1201 } 1202 evaluateResult(index, listener, cmi, false); 1203 } 1204 1205 /** 1206 * Start required evaluation of expression at given index and call back listener when ready. 1207 * If index is MAIN_INDEX, we may also directly display a timeout message. 1208 * Uses longer timeouts than optional evaluation. 1209 * Requires cmi.getMaxChars() != 0. 1210 */ requireResult(long index, EvaluationListener listener, CharMetricsInfo cmi)1211 public void requireResult(long index, EvaluationListener listener, CharMetricsInfo cmi) { 1212 if (cmi.getMaxChars() == 0) { 1213 throw new AssertionError("requireResult called too early"); 1214 } 1215 ExprInfo ei = ensureExprIsCached(index); 1216 if (ei.mResultString == null || (index == MAIN_INDEX && mChangedValue)) { 1217 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. 1220 // The error case doesn't get here. 1221 listener.onCancelled(index); 1222 } else if ((ei.mEvaluator instanceof AsyncEvaluator) 1223 && ((AsyncEvaluator)(ei.mEvaluator)).mRequired) { 1224 // Duplicate request; ignore. 1225 } else { 1226 // (Re)start evaluator in requested mode, i.e. with longer timeout. 1227 cancel(ei, true); 1228 evaluateResult(index, listener, cmi, true); 1229 } 1230 } 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); 1234 } else { 1235 notifyImmediately(index, ei, listener, cmi); 1236 } 1237 } 1238 1239 /** 1240 * Whether this expression has explicitly been evaluated (User pressed "=") 1241 */ hasResult(long index)1242 public boolean hasResult(long index) { 1243 final ExprInfo ei = ensureExprIsCached(index); 1244 return ei.mResultString != null; 1245 } 1246 1247 /** 1248 * Is a reevaluation still in progress? 1249 */ evaluationInProgress(long index)1250 public boolean evaluationInProgress(long index) { 1251 ExprInfo ei = mExprs.get(index); 1252 return ei != null && ei.mEvaluator != null; 1253 } 1254 1255 /** 1256 * Cancel any current background task associated with the given ExprInfo. 1257 * @param quiet suppress cancellation message 1258 * @return true if we cancelled an initial evaluation 1259 */ cancel(ExprInfo expr, boolean quiet)1260 private boolean cancel(ExprInfo expr, boolean quiet) { 1261 if (expr.mEvaluator != null) { 1262 if (quiet && (expr.mEvaluator instanceof AsyncEvaluator)) { 1263 ((AsyncEvaluator)(expr.mEvaluator)).suppressCancelMessage(); 1264 } 1265 // Reevaluation in progress. 1266 if (expr.mVal.get() != null) { 1267 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 { 1273 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; 1285 } 1286 } 1287 return false; 1288 } 1289 1290 /** 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 */ cancel(long index, boolean quiet)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 cancelAll(boolean quiet)1305 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 1313 /** 1314 * Quietly cancel all evaluations associated with expressions other than the main one. 1315 * These are currently the evaluations associated with the history fragment. 1316 */ cancelNonMain()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 /** 1328 * Restore the evaluator state, including the current expression. 1329 */ restoreInstanceState(DataInput in)1330 public void restoreInstanceState(DataInput in) { 1331 mChangedValue = true; 1332 try { 1333 mMainExpr.mDegreeMode = in.readBoolean(); 1334 mMainExpr.mLongTimeout = in.readBoolean(); 1335 mMainExpr.mExpr = new CalculatorExpr(in); 1336 mHasTrigFuncs = hasTrigFuncs(); 1337 } catch (IOException e) { 1338 Log.v("Calculator", "Exception while restoring:\n" + e); 1339 } 1340 } 1341 1342 /** 1343 * Save the evaluator state, including the expression and any saved value. 1344 */ saveInstanceState(DataOutput out)1345 public void saveInstanceState(DataOutput out) { 1346 try { 1347 out.writeBoolean(mMainExpr.mDegreeMode); 1348 out.writeBoolean(mMainExpr.mLongTimeout); 1349 mMainExpr.mExpr.write(out); 1350 } catch (IOException e) { 1351 Log.v("Calculator", "Exception while saving state:\n" + e); 1352 } 1353 } 1354 1355 1356 /** 1357 * Append a button press to the main expression. 1358 * @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 */ append(int id)1362 public boolean append(int id) { 1363 if (id == R.id.fun_10pow) { 1364 add10pow(); // Handled as macro expansion. 1365 return true; 1366 } else { 1367 mChangedValue = mChangedValue || !KeyMaps.isBinary(id); 1368 if (mMainExpr.mExpr.add(id)) { 1369 if (!mHasTrigFuncs) { 1370 mHasTrigFuncs = KeyMaps.isTrigFunc(id); 1371 } 1372 return true; 1373 } else { 1374 return false; 1375 } 1376 } 1377 } 1378 1379 /** 1380 * Delete last taken from main expression. 1381 */ delete()1382 public void delete() { 1383 mChangedValue = true; 1384 mMainExpr.mExpr.delete(); 1385 if (mMainExpr.mExpr.isEmpty()) { 1386 mMainExpr.mLongTimeout = false; 1387 } 1388 mHasTrigFuncs = hasTrigFuncs(); 1389 } 1390 1391 /** 1392 * Set degree mode for main expression. 1393 */ setDegreeMode(boolean degreeMode)1394 public void setDegreeMode(boolean degreeMode) { 1395 mChangedValue = true; 1396 mMainExpr.mDegreeMode = degreeMode; 1397 1398 mSharedPrefs.edit() 1399 .putBoolean(KEY_PREF_DEGREE_MODE, degreeMode) 1400 .apply(); 1401 } 1402 1403 /** 1404 * Return an ExprInfo for a copy of the expression with the given index. 1405 * We remove trailing binary operators in the copy. 1406 * mTimeStamp is not copied. 1407 */ copy(long index, boolean copyValue)1408 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; 1422 } 1423 1424 /** 1425 * Return an ExprInfo corresponding to the sum of the expressions at the 1426 * two indices. 1427 * 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. 1430 * Can return null if evaluation resulted in an error (a very unlikely case). 1431 */ sum(long index1, long index2)1432 private ExprInfo sum(long index1, long index2) { 1433 return generalized_sum(index1, index2, R.id.op_add); 1434 } 1435 1436 /** 1437 * 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 1439 * been previously evaluated. The result is unevaluated. Can return null. 1440 */ difference(long minuendIndex, long subtrahendIndex)1441 private ExprInfo difference(long minuendIndex, long subtrahendIndex) { 1442 return generalized_sum(minuendIndex, subtrahendIndex, R.id.op_sub); 1443 } 1444 generalized_sum(long index1, long index2, int op)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; 1461 } 1462 1463 /** 1464 * Add the expression described by the argument to the database. 1465 * Returns the new row id in the database. 1466 * Fills in timestamp in ei, if it was not previously set. 1467 * If in_history is true, add it with a positive index, so it will appear in the history. 1468 */ addToDB(boolean in_history, ExprInfo ei)1469 private long addToDB(boolean in_history, ExprInfo ei) { 1470 byte[] serializedExpr = ei.mExpr.toBytes(); 1471 ExpressionDB.RowData rd = new ExpressionDB.RowData(serializedExpr, ei.mDegreeMode, 1472 ei.mLongTimeout, 0); 1473 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; 1479 if (resultIndex == MAIN_INDEX) { 1480 throw new AssertionError("Should not store main expression"); 1481 } 1482 mExprs.put(resultIndex, ei); 1483 return resultIndex; 1484 } 1485 1486 /** 1487 * 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. 1489 * 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 */ preserve(long old_index, boolean in_history)1493 public long preserve(long old_index, boolean in_history) { 1494 ExprInfo ei = copy(old_index, true); 1495 if (ei.mResultString == null || ei.mResultString == ERRONEOUS_RESULT) { 1496 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, 1503 * assuming it is already in the database, but may have been lost from the cache. 1504 */ represerve()1505 public void represerve() { 1506 long resultIndex = getMaxIndex(); 1507 // 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); 1512 } 1513 1514 /** 1515 * 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 */ copyMainToHistory()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 /** 1526 * @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 */ getCollapsedExpr(long index)1532 private CalculatorExpr getCollapsedExpr(long index) { 1533 long real_index = isMutableIndex(index) ? preserve(index, false) : index; 1534 final ExprInfo ei = mExprs.get(real_index); 1535 final String rs = ei.mResultString; 1536 // An error can occur here only under extremely unlikely conditions. 1537 // Check anyway, and just refuse. 1538 // 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) { 1541 return null; 1542 } 1543 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. 1552 * 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 */ collapse(long index)1557 public void collapse(long index) { 1558 final boolean longTimeout = mExprs.get(index).mLongTimeout; 1559 final CalculatorExpr abbrvExpr = getCollapsedExpr(index); 1560 clearMain(); 1561 mMainExpr.mExpr.append(abbrvExpr); 1562 mMainExpr.mLongTimeout = longTimeout; 1563 mChangedValue = true; 1564 mHasTrigFuncs = false; // Degree mode no longer affects expression value. 1565 } 1566 1567 /** 1568 * Mark the expression as changed, preventing next evaluation request from being ignored. 1569 */ touch()1570 public void touch() { 1571 mChangedValue = true; 1572 } 1573 1574 private abstract class SetWhenDoneListener implements EvaluationListener { badCall()1575 private void badCall() { 1576 throw new AssertionError("unexpected callback"); 1577 } setNow()1578 abstract void setNow(); 1579 @Override onCancelled(long index)1580 public void onCancelled(long index) {} // Extremely unlikely; leave unset. 1581 @Override onError(long index, int errorId)1582 public void onError(long index, int errorId) {} // Extremely unlikely; leave unset. 1583 @Override onEvaluate(long index, int initPrecOffset, int msdIndex, int lsdOffset, String truncatedWholePart)1584 public void onEvaluate(long index, int initPrecOffset, int msdIndex, int lsdOffset, 1585 String truncatedWholePart) { 1586 setNow(); 1587 } 1588 @Override onReevaluate(long index)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; SetMemoryWhenDoneListener(long index, boolean persist)1597 SetMemoryWhenDoneListener(long index, boolean persist) { 1598 mIndex = index; 1599 mPersist = persist; 1600 } 1601 @Override setNow()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; SetSavedWhenDoneListener(long index)1616 SetSavedWhenDoneListener(long index) { 1617 mIndex = index; 1618 } 1619 @Override setNow()1620 void setNow() { 1621 mSavedIndex = mIndex; 1622 } 1623 } 1624 1625 /** 1626 * Set the local and persistent memory index. 1627 */ setMemoryIndex(long index)1628 private void setMemoryIndex(long index) { 1629 mMemoryIndex = index; 1630 mSharedPrefs.edit() 1631 .putLong(KEY_PREF_MEMORY_INDEX, index) 1632 .apply(); 1633 1634 if (mCallback != null) { 1635 mCallback.onMemoryStateChanged(); 1636 } 1637 } 1638 1639 /** 1640 * Set the local and persistent saved index. 1641 */ setSavedIndex(long index)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 */ setMemoryIndexWhenEvaluated(long index, boolean persist)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 */ setSavedIndexWhenEvaluated(long index)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. 1667 * mExpr is left alone. Return false if result is unavailable. 1668 */ copyToSaved(long index)1669 private boolean copyToSaved(long index) { 1670 if (mExprs.get(index).mResultString == null 1671 || mExprs.get(index).mResultString == ERRONEOUS_RESULT) { 1672 return false; 1673 } 1674 setSavedIndex(isMutableIndex(index) ? preserve(index, false) : index); 1675 return true; 1676 } 1677 1678 /** 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 */ copyToMemory(long index)1682 public void copyToMemory(long index) { 1683 setMemoryIndex(isMutableIndex(index) ? preserve(index, false) : index); 1684 } 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 */ addToMemory(long index)1690 public void addToMemory(long index) { 1691 ExprInfo newEi = sum(mMemoryIndex, index); 1692 if (newEi != null) { 1693 long newIndex = addToDB(false, newEi); 1694 mMemoryIndex = 0; // Invalidate while we're evaluating. 1695 setMemoryIndexWhenEvaluated(newIndex, true /* persist */); 1696 } 1697 } 1698 1699 /** 1700 * 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 */ subtractFromMemory(long index)1703 public void subtractFromMemory(long index) { 1704 ExprInfo newEi = difference(mMemoryIndex, index); 1705 if (newEi != null) { 1706 long newIndex = addToDB(false, newEi); 1707 mMemoryIndex = 0; // Invalidate while we're evaluating. 1708 setMemoryIndexWhenEvaluated(newIndex, true /* persist */); 1709 } 1710 } 1711 1712 /** 1713 * Return index of "saved" expression, or 0. 1714 */ getSavedIndex()1715 public long getSavedIndex() { 1716 return mSavedIndex; 1717 } 1718 1719 /** 1720 * Return index of "memory" expression, or 0. 1721 */ getMemoryIndex()1722 public long getMemoryIndex() { 1723 return mMemoryIndex; 1724 } 1725 uriForSaved()1726 private Uri uriForSaved() { 1727 return new Uri.Builder().scheme("tag") 1728 .encodedOpaquePart(mSavedName) 1729 .build(); 1730 } 1731 1732 /** 1733 * 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. 1735 */ capture(long index)1736 public Uri capture(long index) { 1737 if (!copyToSaved(index)) return null; 1738 // Generate a new (entirely private) URI for this result. 1739 // Attempt to conform to RFC4151, though it's unclear it matters. 1740 final TimeZone tz = TimeZone.getDefault(); 1741 DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); 1742 df.setTimeZone(tz); 1743 final String isoDate = df.format(new Date()); 1744 mSavedName = "calculator2.android.com," + isoDate + ":" 1745 + (new Random().nextInt() & 0x3fffffff); 1746 mSharedPrefs.edit() 1747 .putString(KEY_PREF_SAVED_NAME, mSavedName) 1748 .apply(); 1749 return uriForSaved(); 1750 } 1751 isLastSaved(Uri uri)1752 public boolean isLastSaved(Uri uri) { 1753 return mSavedIndex != 0 && uri.equals(uriForSaved()); 1754 } 1755 1756 /** 1757 * Append the expression at index as a pre-evaluated expression to the main expression. 1758 */ appendExpr(long index)1759 public void appendExpr(long index) { 1760 ExprInfo ei = mExprs.get(index); 1761 mChangedValue = true; 1762 mMainExpr.mLongTimeout |= ei.mLongTimeout; 1763 CalculatorExpr collapsed = getCollapsedExpr(index); 1764 if (collapsed != null) { 1765 mMainExpr.mExpr.append(getCollapsedExpr(index)); 1766 } 1767 } 1768 1769 /** 1770 * Add the power of 10 operator to the main expression. 1771 * This is treated essentially as a macro expansion. 1772 */ add10pow()1773 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. 1778 mMainExpr.mExpr.append(ten); 1779 mMainExpr.mExpr.add(R.id.op_pow); 1780 } 1781 1782 /** 1783 * 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. 1787 */ ensureExprIsCached(long index)1788 private ExprInfo ensureExprIsCached(long index) { 1789 ExprInfo ei = mExprs.get(index); 1790 if (ei != null) { 1791 return ei; 1792 } 1793 if (index == MAIN_INDEX) { 1794 throw new AssertionError("Main expression should be cached"); 1795 } 1796 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; 1802 ei.mLongTimeout = row.longTimeout(); 1803 } 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 getExpr(long index)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 */ getTimeStamp(long index)1819 public long getTimeStamp(long index) { 1820 return ensureExprIsCached(index).mTimeStamp; 1821 } 1822 1823 @Override getDegreeMode(long index)1824 public boolean getDegreeMode(long index) { 1825 return ensureExprIsCached(index).mDegreeMode; 1826 } 1827 1828 @Override getResult(long index)1829 public UnifiedReal getResult(long index) { 1830 return ensureExprIsCached(index).mVal.get(); 1831 } 1832 1833 @Override putResultIfAbsent(long index, UnifiedReal result)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 } 1842 } 1843 1844 /** 1845 * Does the current main expression contain trig functions? 1846 * Might its value depend on DEG/RAD mode? 1847 */ hasTrigFuncs()1848 public boolean hasTrigFuncs() { 1849 return mHasTrigFuncs; 1850 } 1851 1852 /** 1853 * Maximum number of characters in a scientific notation exponent. 1854 */ 1855 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. 1860 * Exponents have syntax E[-]digit* . "E2" and "E-2" are valid. "E+2" and "e2" are not. 1861 * We allow any Unicode digits, and either of the commonly used minus characters. 1862 */ exponentEnd(String s, int offset)1863 public static int exponentEnd(String s, int offset) { 1864 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 } 1873 if (i == len || !Character.isDigit(s.charAt(i))) { 1874 return offset; 1875 } 1876 ++i; 1877 while (i < len && Character.isDigit(s.charAt(i))) { 1878 ++i; 1879 if (i > offset + MAX_EXP_CHARS) { 1880 return offset; 1881 } 1882 } 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. 1889 * The end of the current expression must be a constant. Exponents have the same syntax as 1890 * for exponentEnd(). 1891 */ addExponent(String s, int begin, int end)1892 public void addExponent(String s, int begin, int end) { 1893 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 } 1905 mMainExpr.mExpr.addExponent(sign * exp); 1906 mChangedValue = true; 1907 } 1908 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 */ getExprAsString(long index)1914 public String getExprAsString(long index) { 1915 return getExprAsSpannable(index).toString(); 1916 } 1917 getExprAsSpannable(long index)1918 public Spannable getExprAsSpannable(long index) { 1919 return getExpr(index).toSpannableStringBuilder(mContext); 1920 } 1921 1922 /** 1923 * Generate a String representation of all expressions in the database. 1924 * Debugging only. 1925 */ historyAsString()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 } 1940 1941 /** 1942 * Wait for pending writes to the database to complete. 1943 */ waitForWrites()1944 public void waitForWrites() { 1945 mExprDB.waitForWrites(); 1946 } 1947 1948 /** 1949 * 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 */ destroyEvaluator()1953 public void destroyEvaluator() { 1954 mExprDB.close(); 1955 evaluator = null; 1956 } 1957 1958 public interface Callback { onMemoryStateChanged()1959 void onMemoryStateChanged(); showMessageDialog(@tringRes int title, @StringRes int message, @StringRes int positiveButtonLabel, String tag)1960 void showMessageDialog(@StringRes int title, @StringRes int message, 1961 @StringRes int positiveButtonLabel, String tag); 1962 } 1963 } 1964