diff options
Diffstat (limited to 'src/com/android/calculator2/Evaluator.java')
-rw-r--r-- | src/com/android/calculator2/Evaluator.java | 1379 |
1 files changed, 1065 insertions, 314 deletions
diff --git a/src/com/android/calculator2/Evaluator.java b/src/com/android/calculator2/Evaluator.java index 33960ba..655aa70 100644 --- a/src/com/android/calculator2/Evaluator.java +++ b/src/com/android/calculator2/Evaluator.java @@ -16,52 +16,59 @@ package com.android.calculator2; -import android.app.AlertDialog; import android.content.Context; -import android.content.DialogInterface; import android.content.SharedPreferences; import android.net.Uri; import android.os.AsyncTask; import android.os.Handler; import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.annotation.StringRes; import android.support.annotation.VisibleForTesting; +import android.text.Spannable; import android.util.Log; -import com.hp.creals.CR; // For exception classes. +import com.hp.creals.CR; +import java.io.ByteArrayInputStream; import java.io.DataInput; +import java.io.DataInputStream; import java.io.DataOutput; import java.io.IOException; -import java.math.BigInteger; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Random; import java.util.TimeZone; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; /** - * This implements the calculator evaluation logic. The underlying expression is constructed and - * edited with append(), delete(), and clear(). An evaluation an then be started with a call to - * evaluateAndShowResult() or requireResult(). This starts an asynchronous computation, which - * requests display of the initial result, when available. When initial evaluation is complete, - * it calls the calculator onEvaluate() method. This occurs in a separate event, possibly quite a - * bit later. Once a result has been computed, and before the underlying expression is modified, - * the getString() method may be used to produce Strings that represent approximations to various + * This implements the calculator evaluation logic. + * Logically this maintains a signed integer indexed set of expressions, one of which + * is distinguished as the main expression. + * The main expression is constructed and edited with append(), delete(), etc. + * An evaluation an then be started with a call to evaluateAndNotify() or requireResult(). + * This starts an asynchronous computation, which requests display of the initial result, when + * available. When initial evaluation is complete, it calls the associated listener's + * onEvaluate() method. This occurs in a separate event, possibly quite a bit later. Once a + * result has been computed, and before the underlying expression is modified, the + * getString(index) method may be used to produce Strings that represent approximations to various * precisions. * * Actual expressions being evaluated are represented as {@link CalculatorExpr}s. * - * The Evaluator owns the expression being edited and all associated state needed for evaluating - * it. It provides functionality for saving and restoring this state. However the current - * CalculatorExpr is exposed to the client, and may be directly accessed after cancelling any + * The Evaluator holds the expressions and all associated state needed for evaluating + * them. It provides functionality for saving and restoring this state. However the underlying + * CalculatorExprs are exposed to the client, and may be directly accessed after cancelling any * in-progress computations by invoking the cancelAll() method. * * When evaluation is requested, we invoke the eval() method on the CalculatorExpr from a - * background AsyncTask. A subsequent getString() callback returns immediately, though it may - * return a result containing placeholder ' ' characters. If we had to return palceholder - * characters, we start a background task, which invokes the onReevaluate() callback when it - * completes. In either case, the background task computes the appropriate result digits by - * evaluating the UnifiedReal returned by CalculatorExpr.eval() to the required + * background AsyncTask. A subsequent getString() call for the same expression index returns + * immediately, though it may return a result containing placeholder ' ' characters. If we had to + * return palceholder characters, we start a background task, which invokes the onReevaluate() + * callback when it completes. In either case, the background task computes the appropriate + * result digits by evaluating the UnifiedReal returned by CalculatorExpr.eval() to the required * precision. * * We cache the best decimal approximation we have already computed. We compute generously to @@ -88,15 +95,132 @@ import java.util.TimeZone; * We ensure that only one evaluation of either kind (AsyncEvaluator or AsyncReevaluator) is * running at a time. */ -class Evaluator { +public class Evaluator implements CalculatorExpr.ExprResolver { + + private static Evaluator evaluator; + + public static String TIMEOUT_DIALOG_TAG = "timeout"; + + @NonNull + public static Evaluator getInstance(Context context) { + if (evaluator == null) { + evaluator = new Evaluator(context.getApplicationContext()); + } + return evaluator; + } + + public interface EvaluationListener { + /** + * Called if evaluation was explicitly cancelled or evaluation timed out. + */ + public void onCancelled(long index); + /** + * Called if evaluation resulted in an error. + */ + public void onError(long index, int errorId); + /** + * Called if evaluation completed normally. + * @param index index of expression whose evaluation completed + * @param initPrecOffset the offset used for initial evaluation + * @param msdIndex index of first non-zero digit in the computed result string + * @param lsdOffset offset of last digit in result if result has finite decimal + * expansion + * @param truncatedWholePart the integer part of the result + */ + public void onEvaluate(long index, int initPrecOffset, int msdIndex, int lsdOffset, + String truncatedWholePart); + /** + * Called in response to a reevaluation request, once more precision is available. + * Typically the listener wil respond by calling getString() to retrieve the new + * better approximation. + */ + public void onReevaluate(long index); // More precision is now available; please redraw. + } + + /** + * A query interface for derived information based on character widths. + * This provides information we need to calculate the "preferred precision offset" used + * to display the initial result. It's used to compute the number of digits we can actually + * display. All methods are callable from any thread. + */ + public interface CharMetricsInfo { + /** + * Return the maximum number of (adjusted, digit-width) characters that will fit in the + * result display. May be called asynchronously from non-UI thread. + */ + public int getMaxChars(); + /** + * Return the number of additional digit widths required to add digit separators to + * the supplied string prefix. + * The prefix consists of the first len characters of string s, which is presumed to + * represent a whole number. Callable from non-UI thread. + * Returns zero if metrics information is not yet available. + */ + public float separatorChars(String s, int len); + /** + * Return extra width credit for presence of a decimal point, as fraction of a digit width. + * May be called by non-UI thread. + */ + public float getDecimalCredit(); + /** + * Return extra width credit for absence of ellipsis, as fraction of a digit width. + * May be called by non-UI thread. + */ + public float getNoEllipsisCredit(); + } + + /** + * A CharMetricsInfo that can be used when we are really only interested in computing + * short representations to be embedded on formulas. + */ + private class DummyCharMetricsInfo implements CharMetricsInfo { + @Override + public int getMaxChars() { + return SHORT_TARGET_LENGTH + 10; + } + @Override + public float separatorChars(String s, int len) { + return 0; + } + @Override + public float getDecimalCredit() { + return 0; + } + @Override + public float getNoEllipsisCredit() { + return 0; + } + } + + private final DummyCharMetricsInfo mDummyCharMetricsInfo = new DummyCharMetricsInfo(); + + public static final long MAIN_INDEX = 0; // Index of main expression. + // Once final evaluation of an expression is complete, or when we need to save + // a partial result, we copy the main expression to a non-zero index. + // At that point, the expression no longer changes, and is preserved + // until the entire history is cleared. Only expressions at nonzero indices + // may be embedded in other expressions. + // Each expression index can only have one outstanding evaluation request at a time. + // To avoid conflicts between the history and main View, we copy the main expression + // to allow independent evaluation by both. + public static final long HISTORY_MAIN_INDEX = -1; // Read-only copy of main expression. + // To update e.g. "memory" contents, we copy the corresponding expression to a permanent + // index, and then remember that index. + private long mSavedIndex; // Index of "saved" expression mirroring clipboard. 0 if unused. + private long mMemoryIndex; // Index of "memory" expression. 0 if unused. // When naming variables and fields, "Offset" denotes a character offset in a string // representing a decimal number, where the offset is relative to the decimal point. 1 = // tenths position, -1 = units position. Integer.MAX_VALUE is sometimes used for the offset // of the last digit in an a nonterminating decimal expansion. We use the suffix "Index" to - // denote a zero-based absolute index into such a string. + // denote a zero-based absolute index into such a string. (In other contexts, like above, + // we also use "index" to refer to the key in mExprs below, the list of all known + // expressions.) private static final String KEY_PREF_DEGREE_MODE = "degree_mode"; + private static final String KEY_PREF_SAVED_INDEX = "saved_index"; + private static final String KEY_PREF_MEMORY_INDEX = "memory_index"; + private static final String KEY_PREF_SAVED_NAME = "saved_name"; // The minimum number of extra digits we always try to compute to improve the chance of // producing a correctly-rounded-towards-zero result. The extra digits can be displayed to @@ -123,77 +247,155 @@ class Evaluator { // The largest number of digits to the right of the decimal point to which we will evaluate to // compute proper scientific notation for values close to zero. Chosen to ensure that we - // always to better than IEEE double precision at identifying nonzeros. - // This used only when we cannot a prior determine the most significant digit position, as + // always to better than IEEE double precision at identifying nonzeros. And then some. + // This is used only when we cannot a priori determine the most significant digit position, as // we always can if we have a rational representation. - private static final int MAX_MSD_PREC_OFFSET = 320; + private static final int MAX_MSD_PREC_OFFSET = 1100; // If we can replace an exponent by this many leading zeroes, we do so. Also used in // estimating exponent size for truncating short representation. private static final int EXP_COST = 3; - private final Calculator mCalculator; - private final CalculatorResult mResult; + // Listener that reports changes to the state (empty/filled) of memory. Protected for testing. + private Callback mCallback; - // The current caluclator expression. - private CalculatorExpr mExpr; - - // Last saved expression. Either null or contains a single CalculatorExpr.PreEval node. - private CalculatorExpr mSaved; + // Context for database helper. + private Context mContext; // A hopefully unique name associated with mSaved. private String mSavedName; - // The expression may have changed since the last evaluation in ways that would affect its + // The main expression may have changed since the last evaluation in ways that would affect its // value. private boolean mChangedValue; // The main expression contains trig functions. private boolean mHasTrigFuncs; - private SharedPreferences mSharedPrefs; + public static final int INVALID_MSD = Integer.MAX_VALUE; - private boolean mDegreeMode; // Currently in degree (not radian) mode. + // Used to represent an erroneous result or a required evaluation. Not displayed. + private static final String ERRONEOUS_RESULT = "ERR"; - private final Handler mTimeoutHandler; // Used to schedule evaluation timeouts. + /** + * An individual CalculatorExpr, together with its evaluation state. + * Only the main expression may be changed in-place. The HISTORY_MAIN_INDEX expression is + * periodically reset to be a fresh immutable copy of the main expression. + * All other expressions are only added and never removed. The expressions themselves are + * never modified. + * All fields other than mExpr and mVal are touched only by the UI thread. + * For MAIN_INDEX, mExpr and mVal may change, but are also only ever touched by the UI thread. + * For all other expressions, mExpr does not change once the ExprInfo has been (atomically) + * added to mExprs. mVal may be asynchronously set by any thread, but we take care that it + * does not change after that. mDegreeMode is handled exactly like mExpr. + */ + private class ExprInfo { + public CalculatorExpr mExpr; // The expression itself. + public boolean mDegreeMode; // Evaluating in degree, not radian, mode. + public ExprInfo(CalculatorExpr expr, boolean dm) { + mExpr = expr; + mDegreeMode = dm; + mVal = new AtomicReference<UnifiedReal>(); + } - // The following are valid only if an evaluation completed successfully. - private UnifiedReal mVal; // Value of mExpr as UnifiedReal. + // Currently running expression evaluator, if any. This is either an AsyncEvaluator + // (if mResultString == null or it's obsolete), or an AsyncReevaluator. + // We arrange that only one evaluator is active at a time, in part by maintaining + // two separate ExprInfo structure for the main and history view, so that they can + // arrange for independent evaluators. + public AsyncTask mEvaluator; + + // The remaining fields are valid only if an evaluation completed successfully. + // mVal always points to an AtomicReference, but that may be null. + public AtomicReference<UnifiedReal> mVal; + // We cache the best known decimal result in mResultString. Whenever that is + // non-null, it is computed to exactly mResultStringOffset, which is always > 0. + // Valid only if mResultString is non-null and (for the main expression) !mChangedValue. + // ERRONEOUS_RESULT indicates evaluation resulted in an error. + public String mResultString; + public int mResultStringOffset = 0; + // Number of digits to which (possibly incomplete) evaluation has been requested. + // Only accessed by UI thread. + public int mResultStringOffsetReq = 0; + // Position of most significant digit in current cached result, if determined. This is just + // the index in mResultString holding the msd. + public int mMsdIndex = INVALID_MSD; + // Long timeout needed for evaluation? + public boolean mLongTimeout = false; + public long mTimeStamp; + } - // We cache the best known decimal result in mResultString. Whenever that is - // non-null, it is computed to exactly mResultStringOffset, which is always > 0. - // The cache is filled in by the UI thread. - // Valid only if mResultString is non-null and !mChangedValue. - private String mResultString; - private int mResultStringOffset = 0; + private ConcurrentHashMap<Long, ExprInfo> mExprs = new ConcurrentHashMap<Long, ExprInfo>(); - // Number of digits to which (possibly incomplete) evaluation has been requested. - // Only accessed by UI thread. - private int mResultStringOffsetReq; // Number of digits that have been + // The database holding persistent expressions. + private ExpressionDB mExprDB; - public static final int INVALID_MSD = Integer.MAX_VALUE; + private ExprInfo mMainExpr; // == mExprs.get(MAIN_INDEX) - // Position of most significant digit in current cached result, if determined. This is just - // the index in mResultString holding the msd. - private int mMsdIndex = INVALID_MSD; + private SharedPreferences mSharedPrefs; - // Currently running expression evaluator, if any. - private AsyncEvaluator mEvaluator; + private final Handler mTimeoutHandler; // Used to schedule evaluation timeouts. - // The one and only un-cancelled and currently running reevaluator. Touched only by UI thread. - private AsyncReevaluator mCurrentReevaluator; + private void setMainExpr(ExprInfo expr) { + mMainExpr = expr; + mExprs.put(MAIN_INDEX, expr); + } - Evaluator(Calculator calculator, - CalculatorResult resultDisplay) { - mCalculator = calculator; - mResult = resultDisplay; - mExpr = new CalculatorExpr(); - mSaved = new CalculatorExpr(); + Evaluator(Context context) { + mContext = context; + setMainExpr(new ExprInfo(new CalculatorExpr(), false)); mSavedName = "none"; mTimeoutHandler = new Handler(); - mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(calculator); - mDegreeMode = mSharedPrefs.getBoolean(KEY_PREF_DEGREE_MODE, false); + mExprDB = new ExpressionDB(context); + mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); + mMainExpr.mDegreeMode = mSharedPrefs.getBoolean(KEY_PREF_DEGREE_MODE, false); + long savedIndex = mSharedPrefs.getLong(KEY_PREF_SAVED_INDEX, 0L); + long memoryIndex = mSharedPrefs.getLong(KEY_PREF_MEMORY_INDEX, 0L); + if (savedIndex != 0 && savedIndex != -1 /* Recover from old corruption */) { + setSavedIndexWhenEvaluated(savedIndex); + } + if (memoryIndex != 0 && memoryIndex != -1) { + setMemoryIndexWhenEvaluated(memoryIndex, false /* no need to persist again */); + } + mSavedName = mSharedPrefs.getString(KEY_PREF_SAVED_NAME, "none"); + } + + /** + * Retrieve minimum expression index. + * This is the minimum over all expressions, including uncached ones residing only + * in the data base. If no expressions with negative indices were preserved, this will + * return a small negative predefined constant. + * May be called from any thread, but will block until the database is opened. + */ + public long getMinIndex() { + return mExprDB.getMinIndex(); + } + + /** + * Retrieve maximum expression index. + * This is the maximum over all expressions, including uncached ones residing only + * in the data base. If no expressions with positive indices were preserved, this will + * return 0. + * May be called from any thread, but will block until the database is opened. + */ + public long getMaxIndex() { + return mExprDB.getMaxIndex(); + } + + /** + * Set the Callback for showing dialogs and notifying the UI about memory state changes. + * @param callback + */ + public void setCallback(Callback callback) { + mCallback = callback; + } + + /** + * Does the expression index refer to a transient and mutable expression? + */ + private boolean isMutableIndex(long index) { + return index == MAIN_INDEX || index == HISTORY_MAIN_INDEX; } /** @@ -226,14 +428,9 @@ class Evaluator { } private void displayCancelledMessage() { - new AlertDialog.Builder(mCalculator) - .setMessage(R.string.cancelled) - .setPositiveButton(R.string.dismiss, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface d, int which) { } - }) - .create() - .show(); + if (mCallback != null) { + mCallback.showMessageDialog(0, R.string.cancelled, 0, null); + } } // Timeout handling. @@ -243,16 +440,6 @@ class Evaluator { // destined to fail. /** - * Is a long timeout in effect for the main expression? - */ - private boolean mLongTimeout = false; - - /** - * Is a long timeout in effect for the saved expression? - */ - private boolean mLongSavedTimeout = false; - - /** * Return the timeout in milliseconds. * @param longTimeout a long timeout is in effect */ @@ -267,27 +454,39 @@ class Evaluator { * @param longTimeout a long timeout is in effect */ private int getMaxResultBits(boolean longTimeout) { - return longTimeout ? 350000 : 120000; + return longTimeout ? 700000 : 240000; } /** * Timeout for unrequested, speculative evaluations, in milliseconds. */ - private final long QUICK_TIMEOUT = 1000; + private static final long QUICK_TIMEOUT = 1000; + + /** + * Timeout for non-MAIN expressions. Note that there may be many such evaluations in + * progress on the same thread or core. Thus the evaluation latency may include that needed + * to complete previously enqueued evaluations. Thus the longTimeout flag is not very + * meaningful, and currently ignored. + * Since this is only used for expressions that we have previously successfully evaluated, + * these timeouts hsould never trigger. + */ + private static final long NON_MAIN_TIMEOUT = 100000; /** * Maximum result bit length for unrequested, speculative evaluations. * Also used to bound evaluation precision for small non-zero fractions. */ - private final int QUICK_MAX_RESULT_BITS = 50000; + private static final int QUICK_MAX_RESULT_BITS = 150000; - private void displayTimeoutMessage() { - AlertDialogFragment.showMessageDialog(mCalculator, mCalculator.getString(R.string.timeout), - (mLongTimeout ? null : mCalculator.getString(R.string.ok_remove_timeout))); + private void displayTimeoutMessage(boolean longTimeout) { + if (mCallback != null) { + mCallback.showMessageDialog(R.string.dialog_timeout, R.string.timeout, + longTimeout ? 0 : R.string.ok_remove_timeout, TIMEOUT_DIALOG_TAG); + } } - public void setLongTimeOut() { - mLongTimeout = true; + public void setLongTimeout() { + mMainExpr.mLongTimeout = true; } /** @@ -298,52 +497,94 @@ class Evaluator { */ class AsyncEvaluator extends AsyncTask<Void, Void, InitialResult> { private boolean mDm; // degrees - private boolean mRequired; // Result was requested by user. + public boolean mRequired; // Result was requested by user. private boolean mQuiet; // Suppress cancellation message. private Runnable mTimeoutRunnable = null; - AsyncEvaluator(boolean dm, boolean required) { + private EvaluationListener mListener; // Completion callback. + private CharMetricsInfo mCharMetricsInfo; // Where to get result size information. + private long mIndex; // Expression index. + private ExprInfo mExprInfo; // Current expression. + + AsyncEvaluator(long index, EvaluationListener listener, CharMetricsInfo cmi, boolean dm, + boolean required) { + mIndex = index; + mListener = listener; + mCharMetricsInfo = cmi; mDm = dm; mRequired = required; - mQuiet = !required; + mQuiet = !required || mIndex != MAIN_INDEX; + mExprInfo = mExprs.get(mIndex); + if (mExprInfo.mEvaluator != null) { + throw new AssertionError("Evaluation already in progress!"); + } } - private void handleTimeOut() { + + private void handleTimeout() { + // Runs in UI thread. boolean running = (getStatus() != AsyncTask.Status.FINISHED); if (running && cancel(true)) { - mEvaluator = null; - // Replace mExpr with clone to avoid races if task - // still runs for a while. - mExpr = (CalculatorExpr)mExpr.clone(); - if (mRequired) { + mExprs.get(mIndex).mEvaluator = null; + if (mRequired && mIndex == MAIN_INDEX) { + // Replace mExpr with clone to avoid races if task still runs for a while. + mMainExpr.mExpr = (CalculatorExpr)mMainExpr.mExpr.clone(); suppressCancelMessage(); - displayTimeoutMessage(); + displayTimeoutMessage(mExprInfo.mLongTimeout); } } } + private void suppressCancelMessage() { mQuiet = true; } + @Override protected void onPreExecute() { - long timeout = mRequired ? getTimeout(mLongTimeout) : QUICK_TIMEOUT; + long timeout = mRequired ? getTimeout(mExprInfo.mLongTimeout) : QUICK_TIMEOUT; + if (mIndex != MAIN_INDEX) { + // We evaluated the expression before with the current timeout, so this shouldn't + // ever time out. We evaluate it with a ridiculously long timeout to avoid running + // down the battery if something does go wrong. But we only log such timeouts, and + // invoke the listener with onCancelled. + timeout = NON_MAIN_TIMEOUT; + } mTimeoutRunnable = new Runnable() { @Override public void run() { - handleTimeOut(); + handleTimeout(); } }; + mTimeoutHandler.removeCallbacks(mTimeoutRunnable); mTimeoutHandler.postDelayed(mTimeoutRunnable, timeout); } + /** * Is a computed result too big for decimal conversion? */ private boolean isTooBig(UnifiedReal res) { - int maxBits = mRequired ? getMaxResultBits(mLongTimeout) : QUICK_MAX_RESULT_BITS; + final int maxBits = mRequired ? getMaxResultBits(mExprInfo.mLongTimeout) + : QUICK_MAX_RESULT_BITS; return res.approxWholeNumberBitsGreaterThan(maxBits); } + @Override protected InitialResult doInBackground(Void... nothing) { try { - UnifiedReal res = mExpr.eval(mDm); + // mExpr does not change while we are evaluating; thus it's OK to read here. + UnifiedReal res = mExprInfo.mVal.get(); + if (res == null) { + try { + res = mExprInfo.mExpr.eval(mDm, Evaluator.this); + if (isCancelled()) { + // TODO: This remains very slightly racey. Fix this. + throw new CR.AbortedException(); + } + res = putResultIfAbsent(mIndex, res); + } catch (StackOverflowError e) { + // Absurdly large integer exponents can cause this. There might be other + // examples as well. Treat it as a timeout. + return new InitialResult(R.string.timeout); + } + } if (isTooBig(res)) { // Avoid starting a long uninterruptible decimal conversion. return new InitialResult(R.string.timeout); @@ -370,7 +611,8 @@ class Evaluator { } } final int lsdOffset = getLsdOffset(res, initResult, initResult.indexOf('.')); - final int initDisplayOffset = getPreferredPrec(initResult, msd, lsdOffset); + final int initDisplayOffset = getPreferredPrec(initResult, msd, lsdOffset, + mCharMetricsInfo); final int newPrecOffset = initDisplayOffset + EXTRA_DIGITS; if (newPrecOffset > precOffset) { precOffset = newPrecOffset; @@ -390,50 +632,59 @@ class Evaluator { return new InitialResult(R.string.error_aborted); } } + @Override protected void onPostExecute(InitialResult result) { - mEvaluator = null; + mExprInfo.mEvaluator = null; mTimeoutHandler.removeCallbacks(mTimeoutRunnable); if (result.isError()) { if (result.errorResourceId == R.string.timeout) { - if (mRequired) { - displayTimeoutMessage(); + // Emulating timeout due to large result. + if (mRequired && mIndex == MAIN_INDEX) { + displayTimeoutMessage(mExprs.get(mIndex).mLongTimeout); } - mCalculator.onCancelled(); + mListener.onCancelled(mIndex); } else { - mCalculator.onError(result.errorResourceId); + if (mRequired) { + mExprInfo.mResultString = ERRONEOUS_RESULT; + } + mListener.onError(mIndex, result.errorResourceId); } return; } - mVal = result.val; - mResultString = result.newResultString; - mResultStringOffset = result.newResultStringOffset; - final int dotIndex = mResultString.indexOf('.'); - String truncatedWholePart = mResultString.substring(0, dotIndex); + // mExprInfo.mVal was already set asynchronously by child thread. + mExprInfo.mResultString = result.newResultString; + mExprInfo.mResultStringOffset = result.newResultStringOffset; + final int dotIndex = mExprInfo.mResultString.indexOf('.'); + String truncatedWholePart = mExprInfo.mResultString.substring(0, dotIndex); // Recheck display precision; it may change, since display dimensions may have been // unknow the first time. In that case the initial evaluation precision should have // been conservative. // TODO: Could optimize by remembering display size and checking for change. int initPrecOffset = result.initDisplayOffset; - final int msdIndex = getMsdIndexOf(mResultString); - final int leastDigOffset = getLsdOffset(mVal, mResultString, dotIndex); - final int newInitPrecOffset = getPreferredPrec(mResultString, msdIndex, leastDigOffset); + mExprInfo.mMsdIndex = getMsdIndexOf(mExprInfo.mResultString); + final int leastDigOffset = getLsdOffset(result.val, mExprInfo.mResultString, + dotIndex); + final int newInitPrecOffset = getPreferredPrec(mExprInfo.mResultString, + mExprInfo.mMsdIndex, leastDigOffset, mCharMetricsInfo); if (newInitPrecOffset < initPrecOffset) { initPrecOffset = newInitPrecOffset; } else { // They should be equal. But nothing horrible should happen if they're not. e.g. // because CalculatorResult.MAX_WIDTH was too small. } - mCalculator.onEvaluate(initPrecOffset, msdIndex, leastDigOffset, truncatedWholePart); + mListener.onEvaluate(mIndex, initPrecOffset, mExprInfo.mMsdIndex, leastDigOffset, + truncatedWholePart); } + @Override protected void onCancelled(InitialResult result) { // Invoker resets mEvaluator. mTimeoutHandler.removeCallbacks(mTimeoutRunnable); - if (mRequired && !mQuiet) { + if (!mQuiet) { displayCancelledMessage(); } // Otherwise, if mRequired, timeout processing displayed message. - mCalculator.onCancelled(); + mListener.onCancelled(mIndex); // Just drop the evaluation; Leave expression displayed. return; } @@ -451,7 +702,7 @@ class Evaluator { * but we have failed to prove there aren't such cases. */ @VisibleForTesting - static String unflipZeroes(String oldDigs, int oldPrecOffset, String newDigs, + public static String unflipZeroes(String oldDigs, int oldPrecOffset, String newDigs, int newPrecOffset) { final int oldLen = oldDigs.length(); if (oldDigs.charAt(oldLen - 1) != '9') { @@ -487,13 +738,26 @@ class Evaluator { * Compute new mResultString contents to prec digits to the right of the decimal point. * Ensure that onReevaluate() is called after doing so. If the evaluation fails for reasons * other than a timeout, ensure that onError() is called. + * This assumes that initial evaluation of the expression has been successfully + * completed. */ private class AsyncReevaluator extends AsyncTask<Integer, Void, ReevalResult> { + private long mIndex; // Index of expression to evaluate. + private EvaluationListener mListener; + private ExprInfo mExprInfo; + + AsyncReevaluator(long index, EvaluationListener listener) { + mIndex = index; + mListener = listener; + mExprInfo = mExprs.get(mIndex); + } + @Override protected ReevalResult doInBackground(Integer... prec) { try { final int precOffset = prec[0].intValue(); - return new ReevalResult(mVal.toStringTruncated(precOffset), precOffset); + return new ReevalResult(mExprInfo.mVal.get().toStringTruncated(precOffset), + precOffset); } catch(ArithmeticException e) { return null; } catch(CR.PrecisionOverflowException e) { @@ -511,39 +775,44 @@ class Evaluator { // This should only be possible in the extremely rare case of encountering a // domain error while reevaluating or in case of a precision overflow. We don't // know of a way to get the latter with a plausible amount of user input. - mCalculator.onError(R.string.error_nan); + mExprInfo.mResultString = ERRONEOUS_RESULT; + mListener.onError(mIndex, R.string.error_nan); } else { - if (result.newResultStringOffset < mResultStringOffset) { + if (result.newResultStringOffset < mExprInfo.mResultStringOffset) { throw new AssertionError("Unexpected onPostExecute timing"); } - mResultString = unflipZeroes(mResultString, mResultStringOffset, - result.newResultString, result.newResultStringOffset); - mResultStringOffset = result.newResultStringOffset; - mCalculator.onReevaluate(); + mExprInfo.mResultString = unflipZeroes(mExprInfo.mResultString, + mExprInfo.mResultStringOffset, result.newResultString, + result.newResultStringOffset); + mExprInfo.mResultStringOffset = result.newResultStringOffset; + mListener.onReevaluate(mIndex); } - mCurrentReevaluator = null; + mExprInfo.mEvaluator = null; } // On cancellation we do nothing; invoker should have left no trace of us. } /** - * If necessary, start an evaluation to precOffset. - * Ensure that the display is redrawn when it completes. + * If necessary, start an evaluation of the expression at the given index to precOffset. + * If we start an evaluation the listener is notified on completion. + * Only called if prior evaluation succeeded. */ - private void ensureCachePrec(int precOffset) { - if (mResultString != null && mResultStringOffset >= precOffset - || mResultStringOffsetReq >= precOffset) return; - if (mCurrentReevaluator != null) { + private void ensureCachePrec(long index, int precOffset, EvaluationListener listener) { + ExprInfo ei = mExprs.get(index); + if (ei.mResultString != null && ei.mResultStringOffset >= precOffset + || ei.mResultStringOffsetReq >= precOffset) return; + if (ei.mEvaluator != null) { // Ensure we only have one evaluation running at a time. - mCurrentReevaluator.cancel(true); - mCurrentReevaluator = null; + ei.mEvaluator.cancel(true); + ei.mEvaluator = null; } - mCurrentReevaluator = new AsyncReevaluator(); - mResultStringOffsetReq = precOffset + PRECOMPUTE_DIGITS; - if (mResultString != null) { - mResultStringOffsetReq += mResultStringOffsetReq / PRECOMPUTE_DIVISOR; + AsyncReevaluator reEval = new AsyncReevaluator(index, listener); + ei.mEvaluator = reEval; + ei.mResultStringOffsetReq = precOffset + PRECOMPUTE_DIGITS; + if (ei.mResultString != null) { + ei.mResultStringOffsetReq += ei.mResultStringOffsetReq / PRECOMPUTE_DIVISOR; } - mCurrentReevaluator.execute(mResultStringOffsetReq); + reEval.execute(ei.mResultStringOffsetReq); } /** @@ -555,7 +824,7 @@ class Evaluator { * Integer.MIN_VALUE if we cannot determine. Integer.MAX_VALUE if there is no lsd, * or we cannot determine it. */ - int getLsdOffset(UnifiedReal val, String cache, int decIndex) { + static int getLsdOffset(UnifiedReal val, String cache, int decIndex) { if (val.definitelyZero()) return Integer.MIN_VALUE; int result = val.digitsRequired(); if (result == 0) { @@ -579,12 +848,13 @@ class Evaluator { * @param lastDigitOffset Position of least significant digit (1 = tenths digit) * or Integer.MAX_VALUE. */ - private int getPreferredPrec(String cache, int msd, int lastDigitOffset) { - final int lineLength = mResult.getMaxChars(); + private static int getPreferredPrec(String cache, int msd, int lastDigitOffset, + CharMetricsInfo cm) { + final int lineLength = cm.getMaxChars(); final int wholeSize = cache.indexOf('.'); - final float rawSepChars = mResult.separatorChars(cache, wholeSize); - final float rawSepCharsNoDecimal = rawSepChars - mResult.getNoEllipsisCredit(); - final float rawSepCharsWithDecimal = rawSepCharsNoDecimal - mResult.getDecimalCredit(); + final float rawSepChars = cm.separatorChars(cache, wholeSize); + final float rawSepCharsNoDecimal = rawSepChars - cm.getNoEllipsisCredit(); + final float rawSepCharsWithDecimal = rawSepCharsNoDecimal - cm.getDecimalCredit(); final int sepCharsNoDecimal = (int) Math.ceil(Math.max(rawSepCharsNoDecimal, 0.0f)); final int sepCharsWithDecimal = (int) Math.ceil(Math.max(rawSepCharsWithDecimal, 0.0f)); final int negative = cache.charAt(0) == '-' ? 1 : 0; @@ -646,7 +916,7 @@ class Evaluator { * @param lsdOffset Position of least significant digit in finite representation, * relative to decimal point, or MAX_VALUE. */ - private String getShortString(String cache, int msdIndex, int lsdOffset) { + private static String getShortString(String cache, int msdIndex, int lsdOffset) { // This somewhat mirrors the display formatting code, but // - The constants are different, since we don't want to use the whole display. // - This is an easier problem, since we don't support scrolling and the length @@ -741,25 +1011,26 @@ class Evaluator { } /** - * Return most significant digit index in the currently computed result. + * Return most significant digit index for the result of the expressin at the given index. * Returns an index in the result character array. Return INVALID_MSD if the current result * is too close to zero to determine the result. * Result is almost consistent through reevaluations: It may increase by one, once. */ - private int getMsdIndex() { - if (mMsdIndex != INVALID_MSD) { + private int getMsdIndex(long index) { + ExprInfo ei = mExprs.get(index); + if (ei.mMsdIndex != INVALID_MSD) { // 0.100000... can change to 0.0999999... We may have to correct once by one digit. - if (mResultString.charAt(mMsdIndex) == '0') { - mMsdIndex++; + if (ei.mResultString.charAt(ei.mMsdIndex) == '0') { + ei.mMsdIndex++; } - return mMsdIndex; + return ei.mMsdIndex; } - if (mVal.definitelyZero()) { + if (ei.mVal.get().definitelyZero()) { return INVALID_MSD; // None exists } int result = INVALID_MSD; - if (mResultString != null) { - result = getMsdIndexOf(mResultString); + if (ei.mResultString != null) { + result = ei.mMsdIndex = getMsdIndexOf(ei.mResultString); } return result; } @@ -772,7 +1043,7 @@ class Evaluator { * Return result to precOffset[0] digits to the right of the decimal point. * PrecOffset[0] is updated if the original value is out of range. No exponent or other * indication of precision is added. The result is returned immediately, based on the current - * cache contents, but it may contain question marks for unknown digits. It may also use + * cache contents, but it may contain blanks for unknown digits. It may also use * uncertain digits within EXTRA_DIGITS. If either of those occurred, schedule a reevaluation * and redisplay operation. Uncertain digits never appear to the left of the decimal point. * PrecOffset[0] may be negative to only retrieve digits to the left of the decimal point. @@ -783,32 +1054,35 @@ class Evaluator { * Result uses US conventions; is NOT internationalized. Use getResult() and UnifiedReal * operations to determine whether the result is exact, or whether we dropped trailing digits. * + * @param index Index of expression to approximate * @param precOffset Zeroth element indicates desired and actual precision * @param maxPrecOffset Maximum adjusted precOffset[0] * @param maxDigs Maximum length of result * @param truncated Zeroth element is set if leading nonzero digits were dropped * @param negative Zeroth element is set of the result is negative. + * @param listener EvaluationListener to notify when reevaluation is complete. */ - public String getString(int[] precOffset, int maxPrecOffset, int maxDigs, boolean[] truncated, - boolean[] negative) { + public String getString(long index, int[] precOffset, int maxPrecOffset, int maxDigs, + boolean[] truncated, boolean[] negative, EvaluationListener listener) { + ExprInfo ei = mExprs.get(index); int currentPrecOffset = precOffset[0]; // Make sure we eventually get a complete answer - if (mResultString == null) { - ensureCachePrec(currentPrecOffset + EXTRA_DIGITS); + if (ei.mResultString == null) { + ensureCachePrec(index, currentPrecOffset + EXTRA_DIGITS, listener); // Nothing else to do now; seems to happen on rare occasion with weird user input // timing; Will repair itself in a jiffy. return " "; } else { - ensureCachePrec(currentPrecOffset + EXTRA_DIGITS + mResultString.length() - / EXTRA_DIVISOR); + ensureCachePrec(index, currentPrecOffset + EXTRA_DIGITS + ei.mResultString.length() + / EXTRA_DIVISOR, listener); } // Compute an appropriate substring of mResultString. Pad if necessary. - final int len = mResultString.length(); - final boolean myNegative = mResultString.charAt(0) == '-'; + final int len = ei.mResultString.length(); + final boolean myNegative = ei.mResultString.charAt(0) == '-'; negative[0] = myNegative; // Don't scroll left past leftmost digits in mResultString unless that still leaves an // integer. - int integralDigits = len - mResultStringOffset; + int integralDigits = len - ei.mResultStringOffset; // includes 1 for dec. pt if (myNegative) { --integralDigits; @@ -817,19 +1091,19 @@ class Evaluator { currentPrecOffset = Math.min(Math.max(currentPrecOffset, minPrecOffset), maxPrecOffset); precOffset[0] = currentPrecOffset; - int extraDigs = mResultStringOffset - currentPrecOffset; // trailing digits to drop + int extraDigs = ei.mResultStringOffset - currentPrecOffset; // trailing digits to drop int deficit = 0; // The number of digits we're short if (extraDigs < 0) { extraDigs = 0; - deficit = Math.min(currentPrecOffset - mResultStringOffset, maxDigs); + deficit = Math.min(currentPrecOffset - ei.mResultStringOffset, maxDigs); } int endIndex = len - extraDigs; if (endIndex < 1) { return " "; } int startIndex = Math.max(endIndex + deficit - maxDigs, 0); - truncated[0] = (startIndex > getMsdIndex()); - String result = mResultString.substring(startIndex, endIndex); + truncated[0] = (startIndex > getMsdIndex(index)); + String result = ei.mResultString.substring(startIndex, endIndex); if (deficit > 0) { result += StringUtils.repeat(' ', deficit); // Blank character is replaced during translation. @@ -841,135 +1115,225 @@ class Evaluator { } /** - * Return rational representation of current result, if any. - * Return null if the result is irrational, or we couldn't track the rational value, - * e.g. because the denominator got too big. + * Clear the cache for the main expression. */ - public UnifiedReal getResult() { - return mVal; - } - - private void clearCache() { - mResultString = null; - mResultStringOffset = mResultStringOffsetReq = 0; - mMsdIndex = INVALID_MSD; + private void clearMainCache() { + mMainExpr.mVal.set(null); + mMainExpr.mResultString = null; + mMainExpr.mResultStringOffset = mMainExpr.mResultStringOffsetReq = 0; + mMainExpr.mMsdIndex = INVALID_MSD; } - private void clearPreservingTimeout() { - mExpr.clear(); + public void clearMain() { + mMainExpr.mExpr.clear(); mHasTrigFuncs = false; - clearCache(); + clearMainCache(); + mMainExpr.mLongTimeout = false; } - public void clear() { - clearPreservingTimeout(); - mLongTimeout = false; + public void clearEverything() { + boolean dm = mMainExpr.mDegreeMode; + cancelAll(true); + setSavedIndex(0); + setMemoryIndex(0); + mExprDB.eraseAll(); + mExprs.clear(); + setMainExpr(new ExprInfo(new CalculatorExpr(), dm)); } /** - * Start asynchronous result evaluation of formula. - * Will result in display on completion. + * Start asynchronous evaluation. + * Invoke listener on successful completion. If the result is required, invoke + * onCancelled() if cancelled. + * @param index index of expression to be evaluated. * @param required result was explicitly requested by user. */ - private void evaluateResult(boolean required) { - clearCache(); - mEvaluator = new AsyncEvaluator(mDegreeMode, required); - mEvaluator.execute(); - mChangedValue = false; + private void evaluateResult(long index, EvaluationListener listener, CharMetricsInfo cmi, + boolean required) { + ExprInfo ei = mExprs.get(index); + if (index == MAIN_INDEX) { + clearMainCache(); + } // Otherwise the expression is immutable. + AsyncEvaluator eval = new AsyncEvaluator(index, listener, cmi, ei.mDegreeMode, required); + ei.mEvaluator = eval; + eval.execute(); + if (index == MAIN_INDEX) { + mChangedValue = false; + } } /** - * Start optional evaluation of result and display when ready. - * Can quietly time out without a user-visible display. + * Notify listener of a previously completed evaluation. */ - public void evaluateAndShowResult() { - if (!mChangedValue) { - // Already done or in progress. + void notifyImmediately(long index, ExprInfo ei, EvaluationListener listener, + CharMetricsInfo cmi) { + final int dotIndex = ei.mResultString.indexOf('.'); + final String truncatedWholePart = ei.mResultString.substring(0, dotIndex); + final int leastDigOffset = getLsdOffset(ei.mVal.get(), ei.mResultString, dotIndex); + final int msdIndex = getMsdIndex(index); + final int preferredPrecOffset = getPreferredPrec(ei.mResultString, msdIndex, + leastDigOffset, cmi); + listener.onEvaluate(index, preferredPrecOffset, msdIndex, leastDigOffset, + truncatedWholePart); + } + + /** + * Start optional evaluation of expression and display when ready. + * @param index of expression to be evaluated. + * Can quietly time out without a listener callback. + * No-op if cmi.getMaxChars() == 0. + */ + public void evaluateAndNotify(long index, EvaluationListener listener, CharMetricsInfo cmi) { + if (cmi.getMaxChars() == 0) { + // Probably shouldn't happen. If it does, we didn't promise to do anything anyway. + return; + } + ExprInfo ei = ensureExprIsCached(index); + if (ei.mResultString != null && ei.mResultString != ERRONEOUS_RESULT + && !(index == MAIN_INDEX && mChangedValue)) { + // Already done. Just notify. + notifyImmediately(MAIN_INDEX, mMainExpr, listener, cmi); + return; + } else if (ei.mEvaluator != null) { + // We only allow a single listener per expression, so this request must be redundant. return; } - // In very odd cases, there can be significant latency to evaluate. - // Don't show obsolete result. - mResult.clear(); - evaluateResult(false); + evaluateResult(index, listener, cmi, false); } /** - * Start required evaluation of result and display when ready. - * Will eventually call back mCalculator to display result or error, or display - * a timeout message. Uses longer timeouts than optional evaluation. + * Start required evaluation of expression at given index and call back listener when ready. + * If index is MAIN_INDEX, we may also directly display a timeout message. + * Uses longer timeouts than optional evaluation. + * Requires cmi.getMaxChars() != 0. */ - public void requireResult() { - if (mResultString == null || mChangedValue) { - // Restart evaluator in requested mode, i.e. with longer timeout. - cancelAll(true); - evaluateResult(true); + public void requireResult(long index, EvaluationListener listener, CharMetricsInfo cmi) { + if (cmi.getMaxChars() == 0) { + throw new AssertionError("requireResult called too early"); + } + ExprInfo ei = ensureExprIsCached(index); + if (ei.mResultString == null || (index == MAIN_INDEX && mChangedValue)) { + if (index == HISTORY_MAIN_INDEX) { + // We don't want to compute a result for HISTORY_MAIN_INDEX that was + // not already computed for the main expression. Pretend we timed out. + // The error case doesn't get here. + listener.onCancelled(index); + } else if ((ei.mEvaluator instanceof AsyncEvaluator) + && ((AsyncEvaluator)(ei.mEvaluator)).mRequired) { + // Duplicate request; ignore. + } else { + // (Re)start evaluator in requested mode, i.e. with longer timeout. + cancel(ei, true); + evaluateResult(index, listener, cmi, true); + } + } else if (ei.mResultString == ERRONEOUS_RESULT) { + // Just re-evaluate to generate a new notification. + cancel(ei, true); + evaluateResult(index, listener, cmi, true); } else { - // Notify immediately, reusing existing result. - final int dotIndex = mResultString.indexOf('.'); - final String truncatedWholePart = mResultString.substring(0, dotIndex); - final int leastDigOffset = getLsdOffset(mVal, mResultString, dotIndex); - final int msdIndex = getMsdIndex(); - final int preferredPrecOffset = getPreferredPrec(mResultString, msdIndex, - leastDigOffset); - mCalculator.onEvaluate(preferredPrecOffset, msdIndex, leastDigOffset, - truncatedWholePart); + notifyImmediately(index, ei, listener, cmi); } } /** + * Whether this expression has explicitly been evaluated (User pressed "=") + */ + public boolean hasResult(long index) { + final ExprInfo ei = ensureExprIsCached(index); + return ei.mResultString != null; + } + + /** * Is a reevaluation still in progress? */ - public boolean reevaluationInProgress() { - return mCurrentReevaluator != null; + public boolean evaluationInProgress(long index) { + ExprInfo ei = mExprs.get(index); + return ei != null && ei.mEvaluator != null; } /** - * Cancel all current background tasks. + * Cancel any current background task associated with the given ExprInfo. * @param quiet suppress cancellation message - * @return true if we cancelled an initial evaluation - */ - public boolean cancelAll(boolean quiet) { - if (mCurrentReevaluator != null) { - mCurrentReevaluator.cancel(true); - mResultStringOffsetReq = mResultStringOffset; - // Backgound computation touches only constructive reals. - // OK not to wait. - mCurrentReevaluator = null; - } - if (mEvaluator != null) { - if (quiet) { - mEvaluator.suppressCancelMessage(); + * @return true if we cancelled an initial evaluation + */ + private boolean cancel(ExprInfo expr, boolean quiet) { + if (expr.mEvaluator != null) { + if (quiet && (expr.mEvaluator instanceof AsyncEvaluator)) { + ((AsyncEvaluator)(expr.mEvaluator)).suppressCancelMessage(); + } + // Reevaluation in progress. + if (expr.mVal.get() != null) { + expr.mEvaluator.cancel(true); + expr.mResultStringOffsetReq = expr.mResultStringOffset; + // Backgound computation touches only constructive reals. + // OK not to wait. + expr.mEvaluator = null; + } else { + expr.mEvaluator.cancel(true); + if (expr == mMainExpr) { + // The expression is modifiable, and the AsyncTask is reading it. + // There seems to be no good way to wait for cancellation. + // Give ourselves a new copy to work on instead. + mMainExpr.mExpr = (CalculatorExpr)mMainExpr.mExpr.clone(); + // Approximation of constructive reals should be thread-safe, + // so we can let that continue until it notices the cancellation. + mChangedValue = true; // Didn't do the expected evaluation. + } + expr.mEvaluator = null; + return true; } - mEvaluator.cancel(true); - // There seems to be no good way to wait for cancellation - // to complete, and the evaluation continues to look at - // mExpr, which we will again modify. - // Give ourselves a new copy to work on instead. - mExpr = (CalculatorExpr)mExpr.clone(); - // Approximation of constructive reals should be thread-safe, - // so we can let that continue until it notices the cancellation. - mEvaluator = null; - mChangedValue = true; // Didn't do the expected evaluation. - return true; } return false; } /** - * Restore the evaluator state, including the expression and any saved value. + * Cancel any current background task associated with the given ExprInfo. + * @param quiet suppress cancellation message + * @return true if we cancelled an initial evaluation + */ + public boolean cancel(long index, boolean quiet) + { + ExprInfo ei = mExprs.get(index); + if (ei == null) { + return false; + } else { + return cancel(ei, quiet); + } + } + + public void cancelAll(boolean quiet) { + // TODO: May want to keep active evaluators in a HashSet to avoid traversing + // all expressions we've looked at. + for (ExprInfo expr: mExprs.values()) { + cancel(expr, quiet); + } + } + + /** + * Quietly cancel all evaluations associated with expressions other than the main one. + * These are currently the evaluations associated with the history fragment. + */ + public void cancelNonMain() { + // TODO: May want to keep active evaluators in a HashSet to avoid traversing + // all expressions we've looked at. + for (ExprInfo expr: mExprs.values()) { + if (expr != mMainExpr) { + cancel(expr, true); + } + } + } + + /** + * Restore the evaluator state, including the current expression. */ public void restoreInstanceState(DataInput in) { mChangedValue = true; try { - CalculatorExpr.initExprInput(); - mDegreeMode = in.readBoolean(); - mLongTimeout = in.readBoolean(); - mLongSavedTimeout = in.readBoolean(); - mExpr = new CalculatorExpr(in); - mSavedName = in.readUTF(); - mSaved = new CalculatorExpr(in); - mHasTrigFuncs = mExpr.hasTrigFuncs(); + mMainExpr.mDegreeMode = in.readBoolean(); + mMainExpr.mLongTimeout = in.readBoolean(); + mMainExpr.mExpr = new CalculatorExpr(in); + mHasTrigFuncs = hasTrigFuncs(); } catch (IOException e) { Log.v("Calculator", "Exception while restoring:\n" + e); } @@ -980,13 +1344,9 @@ class Evaluator { */ public void saveInstanceState(DataOutput out) { try { - CalculatorExpr.initExprOutput(); - out.writeBoolean(mDegreeMode); - out.writeBoolean(mLongTimeout); - out.writeBoolean(mLongSavedTimeout); - mExpr.write(out); - out.writeUTF(mSavedName); - mSaved.write(out); + out.writeBoolean(mMainExpr.mDegreeMode); + out.writeBoolean(mMainExpr.mLongTimeout); + mMainExpr.mExpr.write(out); } catch (IOException e) { Log.v("Calculator", "Exception while saving state:\n" + e); } @@ -994,7 +1354,7 @@ class Evaluator { /** - * Append a button press to the current expression. + * Append a button press to the main expression. * @param id Button identifier for the character or operator to be added. * @return false if we rejected the insertion due to obvious syntax issues, and the expression * is unchanged; true otherwise @@ -1005,7 +1365,7 @@ class Evaluator { return true; } else { mChangedValue = mChangedValue || !KeyMaps.isBinary(id); - if (mExpr.add(id)) { + if (mMainExpr.mExpr.add(id)) { if (!mHasTrigFuncs) { mHasTrigFuncs = KeyMaps.isTrigFunc(id); } @@ -1016,49 +1376,190 @@ class Evaluator { } } + /** + * Delete last taken from main expression. + */ public void delete() { mChangedValue = true; - mExpr.delete(); - if (mExpr.isEmpty()) { - mLongTimeout = false; + mMainExpr.mExpr.delete(); + if (mMainExpr.mExpr.isEmpty()) { + mMainExpr.mLongTimeout = false; } - mHasTrigFuncs = mExpr.hasTrigFuncs(); + mHasTrigFuncs = hasTrigFuncs(); } - void setDegreeMode(boolean degreeMode) { + /** + * Set degree mode for main expression. + */ + public void setDegreeMode(boolean degreeMode) { mChangedValue = true; - mDegreeMode = degreeMode; + mMainExpr.mDegreeMode = degreeMode; mSharedPrefs.edit() .putBoolean(KEY_PREF_DEGREE_MODE, degreeMode) .apply(); } - boolean getDegreeMode() { - return mDegreeMode; + /** + * Return an ExprInfo for a copy of the expression with the given index. + * We remove trailing binary operators in the copy. + * mTimeStamp is not copied. + */ + private ExprInfo copy(long index, boolean copyValue) { + ExprInfo fromEi = mExprs.get(index); + ExprInfo ei = new ExprInfo((CalculatorExpr)fromEi.mExpr.clone(), fromEi.mDegreeMode); + while (ei.mExpr.hasTrailingBinary()) { + ei.mExpr.delete(); + } + if (copyValue) { + ei.mVal = new AtomicReference<UnifiedReal>(fromEi.mVal.get()); + ei.mResultString = fromEi.mResultString; + ei.mResultStringOffset = ei.mResultStringOffsetReq = fromEi.mResultStringOffset; + ei.mMsdIndex = fromEi.mMsdIndex; + } + ei.mLongTimeout = fromEi.mLongTimeout; + return ei; + } + + /** + * Return an ExprInfo corresponding to the sum of the expressions at the + * two indices. + * index1 should correspond to an immutable expression, and should thus NOT + * be MAIN_INDEX. Index2 may be MAIN_INDEX. Both expressions are presumed + * to have been evaluated. The result is unevaluated. + * Can return null if evaluation resulted in an error (a very unlikely case). + */ + private ExprInfo sum(long index1, long index2) { + return generalized_sum(index1, index2, R.id.op_add); } /** - * @return the {@link CalculatorExpr} representation of the current result. + * Return an ExprInfo corresponding to the subtraction of the value at the subtrahend index + * from value at the minuend index (minuend - subtrahend = result). Both are presumed to have + * been previously evaluated. The result is unevaluated. Can return null. */ - private CalculatorExpr getResultExpr() { - final int dotIndex = mResultString.indexOf('.'); - final int leastDigOffset = getLsdOffset(mVal, mResultString, dotIndex); - return mExpr.abbreviate(mVal, mDegreeMode, - getShortString(mResultString, getMsdIndexOf(mResultString), leastDigOffset)); + private ExprInfo difference(long minuendIndex, long subtrahendIndex) { + return generalized_sum(minuendIndex, subtrahendIndex, R.id.op_sub); + } + + private ExprInfo generalized_sum(long index1, long index2, int op) { + // TODO: Consider not collapsing expr2, to save database space. + // Note that this is a bit tricky, since our expressions can contain unbalanced lparens. + CalculatorExpr result = new CalculatorExpr(); + CalculatorExpr collapsed1 = getCollapsedExpr(index1); + CalculatorExpr collapsed2 = getCollapsedExpr(index2); + if (collapsed1 == null || collapsed2 == null) { + return null; + } + result.append(collapsed1); + result.add(op); + result.append(collapsed2); + ExprInfo resultEi = new ExprInfo(result, false /* dont care about degrees/radians */); + resultEi.mLongTimeout = mExprs.get(index1).mLongTimeout + || mExprs.get(index2).mLongTimeout; + return resultEi; } /** - * Abbreviate the current expression to a pre-evaluated expression node. + * Add the expression described by the argument to the database. + * Returns the new row id in the database. + * Fills in timestamp in ei, if it was not previously set. + * If in_history is true, add it with a positive index, so it will appear in the history. + */ + private long addToDB(boolean in_history, ExprInfo ei) { + byte[] serializedExpr = ei.mExpr.toBytes(); + ExpressionDB.RowData rd = new ExpressionDB.RowData(serializedExpr, ei.mDegreeMode, + ei.mLongTimeout, 0); + long resultIndex = mExprDB.addRow(!in_history, rd); + if (mExprs.get(resultIndex) != null) { + throw new AssertionError("result slot already occupied! + Slot = " + resultIndex); + } + // Add newly assigned date to the cache. + ei.mTimeStamp = rd.mTimeStamp; + if (resultIndex == MAIN_INDEX) { + throw new AssertionError("Should not store main expression"); + } + mExprs.put(resultIndex, ei); + return resultIndex; + } + + /** + * Preserve a copy of the expression at old_index at a new index. + * This is useful only of old_index is MAIN_INDEX or HISTORY_MAIN_INDEX. + * This assumes that initial evaluation completed suceessfully. + * @param in_history use a positive index so the result appears in the history. + * @return the new index + */ + public long preserve(long old_index, boolean in_history) { + ExprInfo ei = copy(old_index, true); + if (ei.mResultString == null || ei.mResultString == ERRONEOUS_RESULT) { + throw new AssertionError("Preserving unevaluated expression"); + } + return addToDB(in_history, ei); + } + + /** + * Preserve a copy of the current main expression as the most recent history entry, + * assuming it is already in the database, but may have been lost from the cache. + */ + public void represerve() { + long resultIndex = getMaxIndex(); + // This requires database access only if the local state was preserved, but we + // recreated the Evaluator. That excludes the common cases of device rotation, etc. + // TODO: Revisit once we deal with database failures. We could just copy from + // MAIN_INDEX instead, but that loses the timestamp. + ensureExprIsCached(resultIndex); + } + + /** + * Discard previous expression in HISTORY_MAIN_INDEX and replace it by a fresh copy + * of the main expression. Note that the HISTORY_MAIN_INDEX expresssion is not preserved + * in the database or anywhere else; it is always reconstructed when needed. + */ + public void copyMainToHistory() { + cancel(HISTORY_MAIN_INDEX, true /* quiet */); + ExprInfo ei = copy(MAIN_INDEX, true); + mExprs.put(HISTORY_MAIN_INDEX, ei); + } + + /** + * @return the {@link CalculatorExpr} representation of the result of the given + * expression. + * The resulting expression contains a single "token" with the pre-evaluated result. + * The client should ensure that this is never invoked unless initial evaluation of the + * expression has been completed. + */ + private CalculatorExpr getCollapsedExpr(long index) { + long real_index = isMutableIndex(index) ? preserve(index, false) : index; + final ExprInfo ei = mExprs.get(real_index); + final String rs = ei.mResultString; + // An error can occur here only under extremely unlikely conditions. + // Check anyway, and just refuse. + // rs *should* never be null, but it happens. Check as a workaround to protect against + // crashes until we find the root cause (b/34801142) + if (rs == ERRONEOUS_RESULT || rs == null) { + return null; + } + final int dotIndex = rs.indexOf('.'); + final int leastDigOffset = getLsdOffset(ei.mVal.get(), rs, dotIndex); + return ei.mExpr.abbreviate(real_index, + getShortString(rs, getMsdIndexOf(rs), leastDigOffset)); + } + + /** + * Abbreviate the indicated expression to a pre-evaluated expression node, + * and use that as the new main expression. * This should not be called unless the expression was previously evaluated and produced a * non-error result. Pre-evaluated expressions can never represent an expression for which * evaluation to a constructive real diverges. Subsequent re-evaluation will also not * diverge, though it may generate errors of various kinds. E.g. sqrt(-10^-1000) . */ - public void collapse() { - final CalculatorExpr abbrvExpr = getResultExpr(); - clearPreservingTimeout(); - mExpr.append(abbrvExpr); + public void collapse(long index) { + final boolean longTimeout = mExprs.get(index).mLongTimeout; + final CalculatorExpr abbrvExpr = getCollapsedExpr(index); + clearMain(); + mMainExpr.mExpr.append(abbrvExpr); + mMainExpr.mLongTimeout = longTimeout; mChangedValue = true; mHasTrigFuncs = false; // Degree mode no longer affects expression value. } @@ -1070,21 +1571,158 @@ class Evaluator { mChangedValue = true; } + private abstract class SetWhenDoneListener implements EvaluationListener { + private void badCall() { + throw new AssertionError("unexpected callback"); + } + abstract void setNow(); + @Override + public void onCancelled(long index) {} // Extremely unlikely; leave unset. + @Override + public void onError(long index, int errorId) {} // Extremely unlikely; leave unset. + @Override + public void onEvaluate(long index, int initPrecOffset, int msdIndex, int lsdOffset, + String truncatedWholePart) { + setNow(); + } + @Override + public void onReevaluate(long index) { + badCall(); + } + } + + private class SetMemoryWhenDoneListener extends SetWhenDoneListener { + final long mIndex; + final boolean mPersist; + SetMemoryWhenDoneListener(long index, boolean persist) { + mIndex = index; + mPersist = persist; + } + @Override + void setNow() { + if (mMemoryIndex != 0) { + throw new AssertionError("Overwriting nonzero memory index"); + } + if (mPersist) { + setMemoryIndex(mIndex); + } else { + mMemoryIndex = mIndex; + } + } + } + + private class SetSavedWhenDoneListener extends SetWhenDoneListener { + final long mIndex; + SetSavedWhenDoneListener(long index) { + mIndex = index; + } + @Override + void setNow() { + mSavedIndex = mIndex; + } + } + + /** + * Set the local and persistent memory index. + */ + private void setMemoryIndex(long index) { + mMemoryIndex = index; + mSharedPrefs.edit() + .putLong(KEY_PREF_MEMORY_INDEX, index) + .apply(); + + if (mCallback != null) { + mCallback.onMemoryStateChanged(); + } + } + /** - * Abbreviate current expression, and put result in mSaved. + * Set the local and persistent saved index. + */ + private void setSavedIndex(long index) { + mSavedIndex = index; + mSharedPrefs.edit() + .putLong(KEY_PREF_SAVED_INDEX, index) + .apply(); + } + + /** + * Set mMemoryIndex (possibly including the persistent version) to index when we finish + * evaluating the corresponding expression. + */ + void setMemoryIndexWhenEvaluated(long index, boolean persist) { + requireResult(index, new SetMemoryWhenDoneListener(index, persist), mDummyCharMetricsInfo); + } + + /** + * Set mSavedIndex (not the persistent version) to index when we finish evaluating + * the corresponding expression. + */ + void setSavedIndexWhenEvaluated(long index) { + requireResult(index, new SetSavedWhenDoneListener(index), mDummyCharMetricsInfo); + } + + /** + * Save an immutable version of the expression at the given index as the saved value. * mExpr is left alone. Return false if result is unavailable. */ - public boolean collapseToSaved() { - if (mResultString == null) { + private boolean copyToSaved(long index) { + if (mExprs.get(index).mResultString == null + || mExprs.get(index).mResultString == ERRONEOUS_RESULT) { return false; } - final CalculatorExpr abbrvExpr = getResultExpr(); - mSaved.clear(); - mSaved.append(abbrvExpr); - mLongSavedTimeout = mLongTimeout; + setSavedIndex(isMutableIndex(index) ? preserve(index, false) : index); return true; } + /** + * Save an immutable version of the expression at the given index as the "memory" value. + * The expression at index is presumed to have been evaluated. + */ + public void copyToMemory(long index) { + setMemoryIndex(isMutableIndex(index) ? preserve(index, false) : index); + } + + /** + * Save an an expression representing the sum of "memory" and the expression with the + * given index. Make mMemoryIndex point to it when we complete evaluating. + */ + public void addToMemory(long index) { + ExprInfo newEi = sum(mMemoryIndex, index); + if (newEi != null) { + long newIndex = addToDB(false, newEi); + mMemoryIndex = 0; // Invalidate while we're evaluating. + setMemoryIndexWhenEvaluated(newIndex, true /* persist */); + } + } + + /** + * Save an an expression representing the subtraction of the expression with the given index + * from "memory." Make mMemoryIndex point to it when we complete evaluating. + */ + public void subtractFromMemory(long index) { + ExprInfo newEi = difference(mMemoryIndex, index); + if (newEi != null) { + long newIndex = addToDB(false, newEi); + mMemoryIndex = 0; // Invalidate while we're evaluating. + setMemoryIndexWhenEvaluated(newIndex, true /* persist */); + } + } + + /** + * Return index of "saved" expression, or 0. + */ + public long getSavedIndex() { + return mSavedIndex; + } + + /** + * Return index of "memory" expression, or 0. + */ + public long getMemoryIndex() { + return mMemoryIndex; + } + private Uri uriForSaved() { return new Uri.Builder().scheme("tag") .encodedOpaquePart(mSavedName) @@ -1092,12 +1730,11 @@ class Evaluator { } /** - * Collapse the current expression to mSaved and return a URI describing it. - * describing this particular result, so that we can refer to it - * later. + * Save the index expression as the saved location and return a URI describing it. + * The URI is used to distinguish this particular result from others we may generate. */ - public Uri capture() { - if (!collapseToSaved()) return null; + public Uri capture(long index) { + if (!copyToSaved(index)) return null; // Generate a new (entirely private) URI for this result. // Attempt to conform to RFC4151, though it's unclear it matters. final TimeZone tz = TimeZone.getDefault(); @@ -1106,21 +1743,31 @@ class Evaluator { final String isoDate = df.format(new Date()); mSavedName = "calculator2.android.com," + isoDate + ":" + (new Random().nextInt() & 0x3fffffff); + mSharedPrefs.edit() + .putString(KEY_PREF_SAVED_NAME, mSavedName) + .apply(); return uriForSaved(); } public boolean isLastSaved(Uri uri) { - return uri.equals(uriForSaved()); + return mSavedIndex != 0 && uri.equals(uriForSaved()); } - public void appendSaved() { + /** + * Append the expression at index as a pre-evaluated expression to the main expression. + */ + public void appendExpr(long index) { + ExprInfo ei = mExprs.get(index); mChangedValue = true; - mLongTimeout |= mLongSavedTimeout; - mExpr.append(mSaved); + mMainExpr.mLongTimeout |= ei.mLongTimeout; + CalculatorExpr collapsed = getCollapsedExpr(index); + if (collapsed != null) { + mMainExpr.mExpr.append(getCollapsedExpr(index)); + } } /** - * Add the power of 10 operator to the expression. + * Add the power of 10 operator to the main expression. * This is treated essentially as a macro expansion. */ private void add10pow() { @@ -1128,21 +1775,70 @@ class Evaluator { ten.add(R.id.digit_1); ten.add(R.id.digit_0); mChangedValue = true; // For consistency. Reevaluation is probably not useful. - mExpr.append(ten); - mExpr.add(R.id.op_pow); + mMainExpr.mExpr.append(ten); + mMainExpr.mExpr.add(R.id.op_pow); } /** - * Retrieve the main expression being edited. - * It is the callee's reponsibility to call cancelAll to cancel ongoing concurrent - * computations before modifying the result. The resulting expression should only - * be modified by the caller if either the expression value doesn't change, or in - * combination with another add() or delete() call that makes the value change apparent - * to us. - * TODO: Perhaps add functionality so we can keep this private? + * Ensure that the expression with the given index is in mExprs. + * We assume that if it's either already in mExprs or mExprDB. + * When we're done, the expression in mExprs may still contain references to other + * subexpressions that are not yet cached. + */ + private ExprInfo ensureExprIsCached(long index) { + ExprInfo ei = mExprs.get(index); + if (ei != null) { + return ei; + } + if (index == MAIN_INDEX) { + throw new AssertionError("Main expression should be cached"); + } + ExpressionDB.RowData row = mExprDB.getRow(index); + DataInputStream serializedExpr = + new DataInputStream(new ByteArrayInputStream(row.mExpression)); + try { + ei = new ExprInfo(new CalculatorExpr(serializedExpr), row.degreeMode()); + ei.mTimeStamp = row.mTimeStamp; + ei.mLongTimeout = row.longTimeout(); + } catch(IOException e) { + throw new AssertionError("IO Exception without real IO:" + e); + } + ExprInfo newEi = mExprs.putIfAbsent(index, ei); + return newEi == null ? ei : newEi; + } + + @Override + public CalculatorExpr getExpr(long index) { + return ensureExprIsCached(index).mExpr; + } + + /* + * Return timestamp associated with the expression in milliseconds since epoch. + * Yields zero if the expression has not been written to or read from the database. */ - public CalculatorExpr getExpr() { - return mExpr; + public long getTimeStamp(long index) { + return ensureExprIsCached(index).mTimeStamp; + } + + @Override + public boolean getDegreeMode(long index) { + return ensureExprIsCached(index).mDegreeMode; + } + + @Override + public UnifiedReal getResult(long index) { + return ensureExprIsCached(index).mVal.get(); + } + + @Override + public UnifiedReal putResultIfAbsent(long index, UnifiedReal result) { + ExprInfo ei = mExprs.get(index); + if (ei.mVal.compareAndSet(null, result)) { + return result; + } else { + // Cannot change once non-null. + return ei.mVal.get(); + } } /** @@ -1206,7 +1902,62 @@ class Evaluator { for (; i < end; ++i) { exp = 10 * exp + Character.digit(s.charAt(i), 10); } - mExpr.addExponent(sign * exp); + mMainExpr.mExpr.addExponent(sign * exp); mChangedValue = true; } + + /** + * Generate a String representation of the expression at the given index. + * This has the side effect of adding the expression to mExprs. + * The expression must exist in the database. + */ + public String getExprAsString(long index) { + return getExprAsSpannable(index).toString(); + } + + public Spannable getExprAsSpannable(long index) { + return getExpr(index).toSpannableStringBuilder(mContext); + } + + /** + * Generate a String representation of all expressions in the database. + * Debugging only. + */ + public String historyAsString() { + final long startIndex = getMinIndex(); + final long endIndex = getMaxIndex(); + final StringBuilder sb = new StringBuilder(); + for (long i = getMinIndex(); i < ExpressionDB.MAXIMUM_MIN_INDEX; ++i) { + sb.append(i).append(": ").append(getExprAsString(i)).append("\n"); + } + for (long i = 1; i < getMaxIndex(); ++i) { + sb.append(i).append(": ").append(getExprAsString(i)).append("\n"); + } + sb.append("Memory index = ").append(getMemoryIndex()); + sb.append(" Saved index = ").append(getSavedIndex()).append("\n"); + return sb.toString(); + } + + /** + * Wait for pending writes to the database to complete. + */ + public void waitForWrites() { + mExprDB.waitForWrites(); + } + + /** + * Destroy the current evaluator, forcing getEvaluator to allocate a new one. + * This is needed for testing, since Robolectric apparently doesn't let us preserve + * an open databse across tests. Cf. https://github.com/robolectric/robolectric/issues/1890 . + */ + public void destroyEvaluator() { + mExprDB.close(); + evaluator = null; + } + + public interface Callback { + void onMemoryStateChanged(); + void showMessageDialog(@StringRes int title, @StringRes int message, + @StringRes int positiveButtonLabel, String tag); + } } |