summaryrefslogtreecommitdiff
path: root/src/com/android/calculator2
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/calculator2')
-rw-r--r--src/com/android/calculator2/AlertDialogFragment.java134
-rw-r--r--src/com/android/calculator2/AlignedTextView.java76
-rw-r--r--src/com/android/calculator2/BoundedRational.java564
-rw-r--r--src/com/android/calculator2/Calculator.java1538
-rw-r--r--src/com/android/calculator2/CalculatorDisplay.java202
-rw-r--r--src/com/android/calculator2/CalculatorExpr.java1118
-rw-r--r--src/com/android/calculator2/CalculatorFormula.java392
-rw-r--r--src/com/android/calculator2/CalculatorPadViewPager.java252
-rw-r--r--src/com/android/calculator2/CalculatorResult.java1180
-rw-r--r--src/com/android/calculator2/CalculatorScrollView.java83
-rw-r--r--src/com/android/calculator2/DragController.java482
-rw-r--r--src/com/android/calculator2/DragLayout.java357
-rw-r--r--src/com/android/calculator2/Evaluator.java1963
-rw-r--r--src/com/android/calculator2/ExpressionDB.java619
-rw-r--r--src/com/android/calculator2/HistoryAdapter.java222
-rw-r--r--src/com/android/calculator2/HistoryFragment.java243
-rw-r--r--src/com/android/calculator2/HistoryItem.java67
-rw-r--r--src/com/android/calculator2/KeyMaps.java683
-rw-r--r--src/com/android/calculator2/Licenses.java30
-rw-r--r--src/com/android/calculator2/StringUtils.java94
-rw-r--r--src/com/android/calculator2/UnifiedReal.java1285
21 files changed, 0 insertions, 11584 deletions
diff --git a/src/com/android/calculator2/AlertDialogFragment.java b/src/com/android/calculator2/AlertDialogFragment.java
deleted file mode 100644
index 3939a73..0000000
--- a/src/com/android/calculator2/AlertDialogFragment.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.calculator2;
-
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.app.DialogFragment;
-import android.app.FragmentManager;
-import android.content.DialogInterface;
-import android.os.Bundle;
-import androidx.annotation.Nullable;
-import androidx.annotation.StringRes;
-import android.view.LayoutInflater;
-import android.widget.TextView;
-
-/**
- * Display a message with a dismiss putton, and optionally a second button.
- */
-public class AlertDialogFragment extends DialogFragment implements DialogInterface.OnClickListener {
-
- public interface OnClickListener {
- /**
- * This method will be invoked when a button in the dialog is clicked.
- *
- * @param fragment the AlertDialogFragment that received the click
- * @param which the button that was clicked (e.g.
- * {@link DialogInterface#BUTTON_POSITIVE}) or the position
- * of the item clicked
- */
- void onClick(AlertDialogFragment fragment, int which);
- }
-
- private static final String NAME = AlertDialogFragment.class.getName();
- private static final String KEY_MESSAGE = NAME + "_message";
- private static final String KEY_BUTTON_NEGATIVE = NAME + "_button_negative";
- private static final String KEY_BUTTON_POSITIVE = NAME + "_button_positive";
- private static final String KEY_TITLE = NAME + "_title";
-
- /**
- * Convenience method for creating and showing a DialogFragment with the given message and
- * title.
- *
- * @param activity originating Activity
- * @param title resource id for the title string
- * @param message resource id for the displayed message string
- * @param positiveButtonLabel label for second button, if any. If non-null, activity must
- * implement AlertDialogFragment.OnClickListener to respond.
- */
- public static void showMessageDialog(Activity activity, @StringRes int title,
- @StringRes int message, @StringRes int positiveButtonLabel, @Nullable String tag) {
- showMessageDialog(activity, title != 0 ? activity.getString(title) : null,
- activity.getString(message),
- positiveButtonLabel != 0 ? activity.getString(positiveButtonLabel) : null,
- tag);
- }
-
- /**
- * Create and show a DialogFragment with the given message.
- *
- * @param activity originating Activity
- * @param title displayed title, if any
- * @param message displayed message
- * @param positiveButtonLabel label for second button, if any. If non-null, activity must
- * implement AlertDialogFragment.OnClickListener to respond.
- */
- public static void showMessageDialog(Activity activity, @Nullable CharSequence title,
- CharSequence message, @Nullable CharSequence positiveButtonLabel, @Nullable String tag)
- {
- final FragmentManager manager = activity.getFragmentManager();
- if (manager == null || manager.isDestroyed()) {
- return;
- }
- final AlertDialogFragment dialogFragment = new AlertDialogFragment();
- final Bundle args = new Bundle();
- args.putCharSequence(KEY_MESSAGE, message);
- args.putCharSequence(KEY_BUTTON_NEGATIVE, activity.getString(R.string.dismiss));
- if (positiveButtonLabel != null) {
- args.putCharSequence(KEY_BUTTON_POSITIVE, positiveButtonLabel);
- }
- args.putCharSequence(KEY_TITLE, title);
- dialogFragment.setArguments(args);
- dialogFragment.show(manager, tag /* tag */);
- }
-
- public AlertDialogFragment() {
- setStyle(STYLE_NO_TITLE, android.R.attr.alertDialogTheme);
- }
-
- @Override
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- final Bundle args = getArguments() == null ? Bundle.EMPTY : getArguments();
- final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
-
- final LayoutInflater inflater = LayoutInflater.from(builder.getContext());
- final TextView messageView = (TextView) inflater.inflate(
- R.layout.dialog_message, null /* root */);
- messageView.setText(args.getCharSequence(KEY_MESSAGE));
- builder.setView(messageView);
-
- builder.setNegativeButton(args.getCharSequence(KEY_BUTTON_NEGATIVE), null /* listener */);
-
- final CharSequence positiveButtonLabel = args.getCharSequence(KEY_BUTTON_POSITIVE);
- if (positiveButtonLabel != null) {
- builder.setPositiveButton(positiveButtonLabel, this);
- }
-
- builder.setTitle(args.getCharSequence(KEY_TITLE));
-
- return builder.create();
- }
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- final Activity activity = getActivity();
- if (activity instanceof AlertDialogFragment.OnClickListener /* always true */) {
- ((AlertDialogFragment.OnClickListener) activity).onClick(this, which);
- }
- }
-}
diff --git a/src/com/android/calculator2/AlignedTextView.java b/src/com/android/calculator2/AlignedTextView.java
deleted file mode 100644
index 91ad0dd..0000000
--- a/src/com/android/calculator2/AlignedTextView.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.calculator2;
-
-import android.content.Context;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.widget.TextView;
-
-/**
- * Extended {@link TextView} that supports ascent/baseline alignment.
- */
-public class AlignedTextView extends TextView {
-
- private static final String LATIN_CAPITAL_LETTER = "H";
-
- // temporary rect for use during layout
- private final Rect mTempRect = new Rect();
-
- private int mTopPaddingOffset;
- private int mBottomPaddingOffset;
-
- public AlignedTextView(Context context) {
- this(context, null /* attrs */);
- }
-
- public AlignedTextView(Context context, AttributeSet attrs) {
- this(context, attrs, android.R.attr.textViewStyle);
- }
-
- public AlignedTextView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- // Disable any included font padding by default.
- setIncludeFontPadding(false);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- final Paint paint = getPaint();
-
- // Always align text to the default capital letter height.
- paint.getTextBounds(LATIN_CAPITAL_LETTER, 0, 1, mTempRect);
-
- mTopPaddingOffset = Math.min(getPaddingTop(),
- (int) Math.ceil(mTempRect.top - paint.ascent()));
- mBottomPaddingOffset = Math.min(getPaddingBottom(), (int) Math.ceil(paint.descent()));
-
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
-
- @Override
- public int getCompoundPaddingTop() {
- return super.getCompoundPaddingTop() - mTopPaddingOffset;
- }
-
- @Override
- public int getCompoundPaddingBottom() {
- return super.getCompoundPaddingBottom() - mBottomPaddingOffset;
- }
-}
diff --git a/src/com/android/calculator2/BoundedRational.java b/src/com/android/calculator2/BoundedRational.java
deleted file mode 100644
index 16fa581..0000000
--- a/src/com/android/calculator2/BoundedRational.java
+++ /dev/null
@@ -1,564 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.calculator2;
-
-import com.hp.creals.CR;
-
-import java.math.BigInteger;
-import java.util.Objects;
-import java.util.Random;
-
-/**
- * Rational numbers that may turn to null if they get too big.
- * For many operations, if the length of the nuumerator plus the length of the denominator exceeds
- * a maximum size, we simply return null, and rely on our caller do something else.
- * We currently never return null for a pure integer or for a BoundedRational that has just been
- * constructed.
- *
- * We also implement a number of irrational functions. These return a non-null result only when
- * the result is known to be rational.
- */
-public class BoundedRational {
- // TODO: Consider returning null for integers. With some care, large factorials might become
- // much faster.
- // TODO: Maybe eventually make this extend Number?
-
- private static final int MAX_SIZE = 10000; // total, in bits
-
- private final BigInteger mNum;
- private final BigInteger mDen;
-
- public BoundedRational(BigInteger n, BigInteger d) {
- mNum = n;
- mDen = d;
- }
-
- public BoundedRational(BigInteger n) {
- mNum = n;
- mDen = BigInteger.ONE;
- }
-
- public BoundedRational(long n, long d) {
- mNum = BigInteger.valueOf(n);
- mDen = BigInteger.valueOf(d);
- }
-
- public BoundedRational(long n) {
- mNum = BigInteger.valueOf(n);
- mDen = BigInteger.valueOf(1);
- }
-
- /**
- * Produce BoundedRational equal to the given double.
- */
- public static BoundedRational valueOf(double x) {
- final long l = Math.round(x);
- if ((double) l == x && Math.abs(l) <= 1000) {
- return valueOf(l);
- }
- final long allBits = Double.doubleToRawLongBits(Math.abs(x));
- long mantissa = (allBits & ((1L << 52) - 1));
- final int biased_exp = (int)(allBits >>> 52);
- if ((biased_exp & 0x7ff) == 0x7ff) {
- throw new ArithmeticException("Infinity or NaN not convertible to BoundedRational");
- }
- final long sign = x < 0.0 ? -1 : 1;
- int exp = biased_exp - 1075; // 1023 + 52; we treat mantissa as integer.
- if (biased_exp == 0) {
- exp += 1; // Denormal exponent is 1 greater.
- } else {
- mantissa += (1L << 52); // Implied leading one.
- }
- BigInteger num = BigInteger.valueOf(sign * mantissa);
- BigInteger den = BigInteger.ONE;
- if (exp >= 0) {
- num = num.shiftLeft(exp);
- } else {
- den = den.shiftLeft(-exp);
- }
- return new BoundedRational(num, den);
- }
-
- /**
- * Produce BoundedRational equal to the given long.
- */
- public static BoundedRational valueOf(long x) {
- if (x >= -2 && x <= 10) {
- switch((int) x) {
- case -2:
- return MINUS_TWO;
- case -1:
- return MINUS_ONE;
- case 0:
- return ZERO;
- case 1:
- return ONE;
- case 2:
- return TWO;
- case 10:
- return TEN;
- }
- }
- return new BoundedRational(x);
- }
-
- /**
- * Convert to String reflecting raw representation.
- * Debug or log messages only, not pretty.
- */
- public String toString() {
- return mNum.toString() + "/" + mDen.toString();
- }
-
- /**
- * Convert to readable String.
- * Intended for output to user. More expensive, less useful for debugging than
- * toString(). Not internationalized.
- */
- public String toNiceString() {
- final BoundedRational nicer = reduce().positiveDen();
- String result = nicer.mNum.toString();
- if (!nicer.mDen.equals(BigInteger.ONE)) {
- result += "/" + nicer.mDen;
- }
- return result;
- }
-
- public static String toString(BoundedRational r) {
- if (r == null) {
- return "not a small rational";
- }
- return r.toString();
- }
-
- /**
- * Returns a truncated (rounded towards 0) representation of the result.
- * Includes n digits to the right of the decimal point.
- * @param n result precision, >= 0
- */
- public String toStringTruncated(int n) {
- String digits = mNum.abs().multiply(BigInteger.TEN.pow(n)).divide(mDen.abs()).toString();
- int len = digits.length();
- if (len < n + 1) {
- digits = StringUtils.repeat('0', n + 1 - len) + digits;
- len = n + 1;
- }
- return (signum() < 0 ? "-" : "") + digits.substring(0, len - n) + "."
- + digits.substring(len - n);
- }
-
- /**
- * Return a double approximation.
- * The result is correctly rounded to nearest, with ties rounded away from zero.
- * TODO: Should round ties to even.
- */
- public double doubleValue() {
- final int sign = signum();
- if (sign < 0) {
- return -BoundedRational.negate(this).doubleValue();
- }
- // We get the mantissa by dividing the numerator by denominator, after
- // suitably prescaling them so that the integral part of the result contains
- // enough bits. We do the prescaling to avoid any precision loss, so the division result
- // is correctly truncated towards zero.
- final int apprExp = mNum.bitLength() - mDen.bitLength();
- if (apprExp < -1100 || sign == 0) {
- // Bail fast for clearly zero result.
- return 0.0;
- }
- final int neededPrec = apprExp - 80;
- final BigInteger dividend = neededPrec < 0 ? mNum.shiftLeft(-neededPrec) : mNum;
- final BigInteger divisor = neededPrec > 0 ? mDen.shiftLeft(neededPrec) : mDen;
- final BigInteger quotient = dividend.divide(divisor);
- final int qLength = quotient.bitLength();
- int extraBits = qLength - 53;
- int exponent = neededPrec + qLength; // Exponent assuming leading binary point.
- if (exponent >= -1021) {
- // Binary point is actually to right of leading bit.
- --exponent;
- } else {
- // We're in the gradual underflow range. Drop more bits.
- extraBits += (-1022 - exponent) + 1;
- exponent = -1023;
- }
- final BigInteger bigMantissa =
- quotient.add(BigInteger.ONE.shiftLeft(extraBits - 1)).shiftRight(extraBits);
- if (exponent > 1024) {
- return Double.POSITIVE_INFINITY;
- }
- if (exponent > -1023 && bigMantissa.bitLength() != 53
- || exponent <= -1023 && bigMantissa.bitLength() >= 53) {
- throw new AssertionError("doubleValue internal error");
- }
- final long mantissa = bigMantissa.longValue();
- final long bits = (mantissa & ((1l << 52) - 1)) | (((long) exponent + 1023) << 52);
- return Double.longBitsToDouble(bits);
- }
-
- public CR crValue() {
- return CR.valueOf(mNum).divide(CR.valueOf(mDen));
- }
-
- public int intValue() {
- BoundedRational reduced = reduce();
- if (!reduced.mDen.equals(BigInteger.ONE)) {
- throw new ArithmeticException("intValue of non-int");
- }
- return reduced.mNum.intValue();
- }
-
- // Approximate number of bits to left of binary point.
- // Negative indicates leading zeroes to the right of binary point.
- public int wholeNumberBits() {
- if (mNum.signum() == 0) {
- return Integer.MIN_VALUE;
- } else {
- return mNum.bitLength() - mDen.bitLength();
- }
- }
-
- /**
- * Is this number too big for us to continue with rational arithmetic?
- * We return fals for integers on the assumption that we have no better fallback.
- */
- private boolean tooBig() {
- if (mDen.equals(BigInteger.ONE)) {
- return false;
- }
- return (mNum.bitLength() + mDen.bitLength() > MAX_SIZE);
- }
-
- /**
- * Return an equivalent fraction with a positive denominator.
- */
- private BoundedRational positiveDen() {
- if (mDen.signum() > 0) {
- return this;
- }
- return new BoundedRational(mNum.negate(), mDen.negate());
- }
-
- /**
- * Return an equivalent fraction in lowest terms.
- * Denominator sign may remain negative.
- */
- private BoundedRational reduce() {
- if (mDen.equals(BigInteger.ONE)) {
- return this; // Optimization only
- }
- final BigInteger divisor = mNum.gcd(mDen);
- return new BoundedRational(mNum.divide(divisor), mDen.divide(divisor));
- }
-
- static Random sReduceRng = new Random();
-
- /**
- * Return a possibly reduced version of r that's not tooBig().
- * Return null if none exists.
- */
- private static BoundedRational maybeReduce(BoundedRational r) {
- if (r == null) return null;
- // Reduce randomly, with 1/16 probability, or if the result is too big.
- if (!r.tooBig() && (sReduceRng.nextInt() & 0xf) != 0) {
- return r;
- }
- BoundedRational result = r.positiveDen();
- result = result.reduce();
- if (!result.tooBig()) {
- return result;
- }
- return null;
- }
-
- public int compareTo(BoundedRational r) {
- // Compare by multiplying both sides by denominators, invert result if denominator product
- // was negative.
- return mNum.multiply(r.mDen).compareTo(r.mNum.multiply(mDen)) * mDen.signum()
- * r.mDen.signum();
- }
-
- public int signum() {
- return mNum.signum() * mDen.signum();
- }
-
- @Override
- public int hashCode() {
- // Note that this may be too expensive to be useful.
- BoundedRational reduced = reduce().positiveDen();
- return Objects.hash(reduced.mNum, reduced.mDen);
- }
-
- @Override
- public boolean equals(Object r) {
- return r != null && r instanceof BoundedRational && compareTo((BoundedRational) r) == 0;
- }
-
- // We use static methods for arithmetic, so that we can easily handle the null case. We try
- // to catch domain errors whenever possible, sometimes even when one of the arguments is null,
- // but not relevant.
-
- /**
- * Returns equivalent BigInteger result if it exists, null if not.
- */
- public static BigInteger asBigInteger(BoundedRational r) {
- if (r == null) {
- return null;
- }
- final BigInteger[] quotAndRem = r.mNum.divideAndRemainder(r.mDen);
- if (quotAndRem[1].signum() == 0) {
- return quotAndRem[0];
- } else {
- return null;
- }
- }
- public static BoundedRational add(BoundedRational r1, BoundedRational r2) {
- if (r1 == null || r2 == null) {
- return null;
- }
- final BigInteger den = r1.mDen.multiply(r2.mDen);
- final BigInteger num = r1.mNum.multiply(r2.mDen).add(r2.mNum.multiply(r1.mDen));
- return maybeReduce(new BoundedRational(num,den));
- }
-
- /**
- * Return the argument, but with the opposite sign.
- * Returns null only for a null argument.
- */
- public static BoundedRational negate(BoundedRational r) {
- if (r == null) {
- return null;
- }
- return new BoundedRational(r.mNum.negate(), r.mDen);
- }
-
- public static BoundedRational subtract(BoundedRational r1, BoundedRational r2) {
- return add(r1, negate(r2));
- }
-
- /**
- * Return product of r1 and r2 without reducing the result.
- */
- private static BoundedRational rawMultiply(BoundedRational r1, BoundedRational r2) {
- // It's tempting but marginally unsound to reduce 0 * null to 0. The null could represent
- // an infinite value, for which we failed to throw an exception because it was too big.
- if (r1 == null || r2 == null) {
- return null;
- }
- // Optimize the case of our special ONE constant, since that's cheap and somewhat frequent.
- if (r1 == ONE) {
- return r2;
- }
- if (r2 == ONE) {
- return r1;
- }
- final BigInteger num = r1.mNum.multiply(r2.mNum);
- final BigInteger den = r1.mDen.multiply(r2.mDen);
- return new BoundedRational(num,den);
- }
-
- public static BoundedRational multiply(BoundedRational r1, BoundedRational r2) {
- return maybeReduce(rawMultiply(r1, r2));
- }
-
- public static class ZeroDivisionException extends ArithmeticException {
- public ZeroDivisionException() {
- super("Division by zero");
- }
- }
-
- /**
- * Return the reciprocal of r (or null if the argument was null).
- */
- public static BoundedRational inverse(BoundedRational r) {
- if (r == null) {
- return null;
- }
- if (r.mNum.signum() == 0) {
- throw new ZeroDivisionException();
- }
- return new BoundedRational(r.mDen, r.mNum);
- }
-
- public static BoundedRational divide(BoundedRational r1, BoundedRational r2) {
- return multiply(r1, inverse(r2));
- }
-
- public static BoundedRational sqrt(BoundedRational r) {
- // Return non-null if numerator and denominator are small perfect squares.
- if (r == null) {
- return null;
- }
- r = r.positiveDen().reduce();
- if (r.mNum.signum() < 0) {
- throw new ArithmeticException("sqrt(negative)");
- }
- final BigInteger num_sqrt = BigInteger.valueOf(Math.round(Math.sqrt(r.mNum.doubleValue())));
- if (!num_sqrt.multiply(num_sqrt).equals(r.mNum)) {
- return null;
- }
- final BigInteger den_sqrt = BigInteger.valueOf(Math.round(Math.sqrt(r.mDen.doubleValue())));
- if (!den_sqrt.multiply(den_sqrt).equals(r.mDen)) {
- return null;
- }
- return new BoundedRational(num_sqrt, den_sqrt);
- }
-
- public final static BoundedRational ZERO = new BoundedRational(0);
- public final static BoundedRational HALF = new BoundedRational(1,2);
- public final static BoundedRational MINUS_HALF = new BoundedRational(-1,2);
- public final static BoundedRational THIRD = new BoundedRational(1,3);
- public final static BoundedRational QUARTER = new BoundedRational(1,4);
- public final static BoundedRational SIXTH = new BoundedRational(1,6);
- public final static BoundedRational ONE = new BoundedRational(1);
- public final static BoundedRational MINUS_ONE = new BoundedRational(-1);
- public final static BoundedRational TWO = new BoundedRational(2);
- public final static BoundedRational MINUS_TWO = new BoundedRational(-2);
- public final static BoundedRational TEN = new BoundedRational(10);
- public final static BoundedRational TWELVE = new BoundedRational(12);
- public final static BoundedRational THIRTY = new BoundedRational(30);
- public final static BoundedRational MINUS_THIRTY = new BoundedRational(-30);
- public final static BoundedRational FORTY_FIVE = new BoundedRational(45);
- public final static BoundedRational MINUS_FORTY_FIVE = new BoundedRational(-45);
- public final static BoundedRational NINETY = new BoundedRational(90);
- public final static BoundedRational MINUS_NINETY = new BoundedRational(-90);
-
- private static final BigInteger BIG_TWO = BigInteger.valueOf(2);
- private static final BigInteger BIG_MINUS_ONE = BigInteger.valueOf(-1);
-
- /**
- * Compute integral power of this, assuming this has been reduced and exp is >= 0.
- */
- private BoundedRational rawPow(BigInteger exp) {
- if (exp.equals(BigInteger.ONE)) {
- return this;
- }
- if (exp.and(BigInteger.ONE).intValue() == 1) {
- return rawMultiply(rawPow(exp.subtract(BigInteger.ONE)), this);
- }
- if (exp.signum() == 0) {
- return ONE;
- }
- BoundedRational tmp = rawPow(exp.shiftRight(1));
- if (Thread.interrupted()) {
- throw new CR.AbortedException();
- }
- BoundedRational result = rawMultiply(tmp, tmp);
- if (result == null || result.tooBig()) {
- return null;
- }
- return result;
- }
-
- /**
- * Compute an integral power of this.
- */
- public BoundedRational pow(BigInteger exp) {
- int expSign = exp.signum();
- if (expSign == 0) {
- // Questionable if base has undefined or zero value.
- // java.lang.Math.pow() returns 1 anyway, so we do the same.
- return BoundedRational.ONE;
- }
- if (exp.equals(BigInteger.ONE)) {
- return this;
- }
- // Reducing once at the beginning means there's no point in reducing later.
- BoundedRational reduced = reduce().positiveDen();
- // First handle cases in which huge exponents could give compact results.
- if (reduced.mDen.equals(BigInteger.ONE)) {
- if (reduced.mNum.equals(BigInteger.ZERO)) {
- return ZERO;
- }
- if (reduced.mNum.equals(BigInteger.ONE)) {
- return ONE;
- }
- if (reduced.mNum.equals(BIG_MINUS_ONE)) {
- if (exp.testBit(0)) {
- return MINUS_ONE;
- } else {
- return ONE;
- }
- }
- }
- if (exp.bitLength() > 1000) {
- // Stack overflow is likely; a useful rational result is not.
- return null;
- }
- if (expSign < 0) {
- return inverse(reduced).rawPow(exp.negate());
- } else {
- return reduced.rawPow(exp);
- }
- }
-
- public static BoundedRational pow(BoundedRational base, BoundedRational exp) {
- if (exp == null) {
- return null;
- }
- if (base == null) {
- return null;
- }
- exp = exp.reduce().positiveDen();
- if (!exp.mDen.equals(BigInteger.ONE)) {
- return null;
- }
- return base.pow(exp.mNum);
- }
-
-
- private static final BigInteger BIG_FIVE = BigInteger.valueOf(5);
-
- /**
- * Return the number of decimal digits to the right of the decimal point required to represent
- * the argument exactly.
- * Return Integer.MAX_VALUE if that's not possible. Never returns a value less than zero, even
- * if r is a power of ten.
- */
- public static int digitsRequired(BoundedRational r) {
- if (r == null) {
- return Integer.MAX_VALUE;
- }
- int powersOfTwo = 0; // Max power of 2 that divides denominator
- int powersOfFive = 0; // Max power of 5 that divides denominator
- // Try the easy case first to speed things up.
- if (r.mDen.equals(BigInteger.ONE)) {
- return 0;
- }
- r = r.reduce();
- BigInteger den = r.mDen;
- if (den.bitLength() > MAX_SIZE) {
- return Integer.MAX_VALUE;
- }
- while (!den.testBit(0)) {
- ++powersOfTwo;
- den = den.shiftRight(1);
- }
- while (den.mod(BIG_FIVE).signum() == 0) {
- ++powersOfFive;
- den = den.divide(BIG_FIVE);
- }
- // If the denominator has a factor of other than 2 or 5 (the divisors of 10), the decimal
- // expansion does not terminate. Multiplying the fraction by any number of powers of 10
- // will not cancel the demoniator. (Recall the fraction was in lowest terms to start
- // with.) Otherwise the powers of 10 we need to cancel the denominator is the larger of
- // powersOfTwo and powersOfFive.
- if (!den.equals(BigInteger.ONE) && !den.equals(BIG_MINUS_ONE)) {
- return Integer.MAX_VALUE;
- }
- return Math.max(powersOfTwo, powersOfFive);
- }
-}
diff --git a/src/com/android/calculator2/Calculator.java b/src/com/android/calculator2/Calculator.java
deleted file mode 100644
index 81ab1f6..0000000
--- a/src/com/android/calculator2/Calculator.java
+++ /dev/null
@@ -1,1538 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// TODO: Copy & more general paste in formula? Note that this requires
-// great care: Currently the text version of a displayed formula
-// is not directly useful for re-evaluating the formula later, since
-// it contains ellipses representing subexpressions evaluated with
-// a different degree mode. Rather than supporting copy from the
-// formula window, we may eventually want to support generation of a
-// more useful text version in a separate window. It's not clear
-// this is worth the added (code and user) complexity.
-
-package com.android.calculator2;
-
-import android.animation.Animator;
-import android.animation.Animator.AnimatorListener;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
-import android.app.ActionBar;
-import android.app.Activity;
-import android.app.Fragment;
-import android.app.FragmentManager;
-import android.app.FragmentTransaction;
-import android.content.ClipData;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.graphics.Color;
-import android.graphics.Rect;
-import android.net.Uri;
-import android.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.annotation.StringRes;
-import androidx.core.content.ContextCompat;
-import androidx.viewpager.widget.ViewPager;
-import android.text.Editable;
-import android.text.SpannableStringBuilder;
-import android.text.Spanned;
-import android.text.TextUtils;
-import android.text.TextWatcher;
-import android.text.style.ForegroundColorSpan;
-import android.util.Log;
-import android.util.Property;
-import android.view.ActionMode;
-import android.view.KeyCharacterMap;
-import android.view.KeyEvent;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.View.OnLongClickListener;
-import android.view.ViewAnimationUtils;
-import android.view.ViewGroupOverlay;
-import android.view.ViewTreeObserver;
-import android.view.animation.AccelerateDecelerateInterpolator;
-import android.widget.HorizontalScrollView;
-import android.widget.TextView;
-import android.widget.Toolbar;
-
-import com.android.calculator2.CalculatorFormula.OnTextSizeChangeListener;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.ObjectInput;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutput;
-import java.io.ObjectOutputStream;
-import java.text.DecimalFormatSymbols;
-
-import static com.android.calculator2.CalculatorFormula.OnFormulaContextMenuClickListener;
-
-public class Calculator extends Activity
- implements OnTextSizeChangeListener, OnLongClickListener,
- AlertDialogFragment.OnClickListener, Evaluator.EvaluationListener /* for main result */,
- DragLayout.CloseCallback, DragLayout.DragCallback {
-
- private static final String TAG = "Calculator";
- /**
- * Constant for an invalid resource id.
- */
- public static final int INVALID_RES_ID = -1;
-
- private enum CalculatorState {
- INPUT, // Result and formula both visible, no evaluation requested,
- // Though result may be visible on bottom line.
- EVALUATE, // Both visible, evaluation requested, evaluation/animation incomplete.
- // Not used for instant result evaluation.
- INIT, // Very temporary state used as alternative to EVALUATE
- // during reinitialization. Do not animate on completion.
- INIT_FOR_RESULT, // Identical to INIT, but evaluation is known to terminate
- // with result, and current expression has been copied to history.
- ANIMATE, // Result computed, animation to enlarge result window in progress.
- RESULT, // Result displayed, formula invisible.
- // If we are in RESULT state, the formula was evaluated without
- // error to initial precision.
- // The current formula is now also the last history entry.
- ERROR // Error displayed: Formula visible, result shows error message.
- // Display similar to INPUT state.
- }
- // Normal transition sequence is
- // INPUT -> EVALUATE -> ANIMATE -> RESULT (or ERROR) -> INPUT
- // A RESULT -> ERROR transition is possible in rare corner cases, in which
- // a higher precision evaluation exposes an error. This is possible, since we
- // initially evaluate assuming we were given a well-defined problem. If we
- // were actually asked to compute sqrt(<extremely tiny negative number>) we produce 0
- // unless we are asked for enough precision that we can distinguish the argument from zero.
- // ERROR and RESULT are translated to INIT or INIT_FOR_RESULT state if the application
- // is restarted in that state. This leads us to recompute and redisplay the result
- // ASAP. We avoid saving the ANIMATE state or activating history in that state.
- // In INIT_FOR_RESULT, and RESULT state, a copy of the current
- // expression has been saved in the history db; in the other non-ANIMATE states,
- // it has not.
- // TODO: Possibly save a bit more information, e.g. its initial display string
- // or most significant digit position, to speed up restart.
-
- private final Property<TextView, Integer> TEXT_COLOR =
- new Property<TextView, Integer>(Integer.class, "textColor") {
- @Override
- public Integer get(TextView textView) {
- return textView.getCurrentTextColor();
- }
-
- @Override
- public void set(TextView textView, Integer textColor) {
- textView.setTextColor(textColor);
- }
- };
-
- private static final String NAME = "Calculator";
- private static final String KEY_DISPLAY_STATE = NAME + "_display_state";
- private static final String KEY_UNPROCESSED_CHARS = NAME + "_unprocessed_chars";
- /**
- * Associated value is a byte array holding the evaluator state.
- */
- private static final String KEY_EVAL_STATE = NAME + "_eval_state";
- private static final String KEY_INVERSE_MODE = NAME + "_inverse_mode";
- /**
- * Associated value is an boolean holding the visibility state of the toolbar.
- */
- private static final String KEY_SHOW_TOOLBAR = NAME + "_show_toolbar";
-
- private final ViewTreeObserver.OnPreDrawListener mPreDrawListener =
- new ViewTreeObserver.OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
- final ViewTreeObserver observer = mFormulaContainer.getViewTreeObserver();
- if (observer.isAlive()) {
- observer.removeOnPreDrawListener(this);
- }
- return false;
- }
- };
-
- private final Evaluator.Callback mEvaluatorCallback = new Evaluator.Callback() {
- @Override
- public void onMemoryStateChanged() {
- mFormulaText.onMemoryStateChanged();
- }
-
- @Override
- public void showMessageDialog(@StringRes int title, @StringRes int message,
- @StringRes int positiveButtonLabel, String tag) {
- AlertDialogFragment.showMessageDialog(Calculator.this, title, message,
- positiveButtonLabel, tag);
-
- }
- };
-
- private final OnDisplayMemoryOperationsListener mOnDisplayMemoryOperationsListener =
- new OnDisplayMemoryOperationsListener() {
- @Override
- public boolean shouldDisplayMemory() {
- return mEvaluator.getMemoryIndex() != 0;
- }
- };
-
- private final OnFormulaContextMenuClickListener mOnFormulaContextMenuClickListener =
- new OnFormulaContextMenuClickListener() {
- @Override
- public boolean onPaste(ClipData clip) {
- final ClipData.Item item = clip.getItemCount() == 0 ? null : clip.getItemAt(0);
- if (item == null) {
- // nothing to paste, bail early...
- return false;
- }
-
- // Check if the item is a previously copied result, otherwise paste as raw text.
- final Uri uri = item.getUri();
- if (uri != null && mEvaluator.isLastSaved(uri)) {
- clearIfNotInputState();
- mEvaluator.appendExpr(mEvaluator.getSavedIndex());
- redisplayAfterFormulaChange();
- } else {
- addChars(item.coerceToText(Calculator.this).toString(), false);
- }
- return true;
- }
-
- @Override
- public void onMemoryRecall() {
- clearIfNotInputState();
- long memoryIndex = mEvaluator.getMemoryIndex();
- if (memoryIndex != 0) {
- mEvaluator.appendExpr(mEvaluator.getMemoryIndex());
- redisplayAfterFormulaChange();
- }
- }
- };
-
-
- private final TextWatcher mFormulaTextWatcher = new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
- }
-
- @Override
- public void onTextChanged(CharSequence charSequence, int start, int count, int after) {
- }
-
- @Override
- public void afterTextChanged(Editable editable) {
- final ViewTreeObserver observer = mFormulaContainer.getViewTreeObserver();
- if (observer.isAlive()) {
- observer.removeOnPreDrawListener(mPreDrawListener);
- observer.addOnPreDrawListener(mPreDrawListener);
- }
- }
- };
-
- private CalculatorState mCurrentState;
- private Evaluator mEvaluator;
-
- private CalculatorDisplay mDisplayView;
- private TextView mModeView;
- private CalculatorFormula mFormulaText;
- private CalculatorResult mResultText;
- private HorizontalScrollView mFormulaContainer;
- private DragLayout mDragLayout;
-
- private ViewPager mPadViewPager;
- private View mDeleteButton;
- private View mClearButton;
- private View mEqualButton;
- private View mMainCalculator;
-
- private TextView mInverseToggle;
- private TextView mModeToggle;
-
- private View[] mInvertibleButtons;
- private View[] mInverseButtons;
-
- private View mCurrentButton;
- private Animator mCurrentAnimator;
-
- // Characters that were recently entered at the end of the display that have not yet
- // been added to the underlying expression.
- private String mUnprocessedChars = null;
-
- // Color to highlight unprocessed characters from physical keyboard.
- // TODO: should probably match this to the error color?
- private ForegroundColorSpan mUnprocessedColorSpan = new ForegroundColorSpan(Color.RED);
-
- // Whether the display is one line.
- private boolean mIsOneLine;
-
- /**
- * Map the old saved state to a new state reflecting requested result reevaluation.
- */
- private CalculatorState mapFromSaved(CalculatorState savedState) {
- switch (savedState) {
- case RESULT:
- case INIT_FOR_RESULT:
- // Evaluation is expected to terminate normally.
- return CalculatorState.INIT_FOR_RESULT;
- case ERROR:
- case INIT:
- return CalculatorState.INIT;
- case EVALUATE:
- case INPUT:
- return savedState;
- default: // Includes ANIMATE state.
- throw new AssertionError("Impossible saved state");
- }
- }
-
- /**
- * Restore Evaluator state and mCurrentState from savedInstanceState.
- * Return true if the toolbar should be visible.
- */
- private void restoreInstanceState(Bundle savedInstanceState) {
- final CalculatorState savedState = CalculatorState.values()[
- savedInstanceState.getInt(KEY_DISPLAY_STATE,
- CalculatorState.INPUT.ordinal())];
- setState(savedState);
- CharSequence unprocessed = savedInstanceState.getCharSequence(KEY_UNPROCESSED_CHARS);
- if (unprocessed != null) {
- mUnprocessedChars = unprocessed.toString();
- }
- byte[] state = savedInstanceState.getByteArray(KEY_EVAL_STATE);
- if (state != null) {
- try (ObjectInput in = new ObjectInputStream(new ByteArrayInputStream(state))) {
- mEvaluator.restoreInstanceState(in);
- } catch (Throwable ignored) {
- // When in doubt, revert to clean state
- mCurrentState = CalculatorState.INPUT;
- mEvaluator.clearMain();
- }
- }
- if (savedInstanceState.getBoolean(KEY_SHOW_TOOLBAR, true)) {
- showAndMaybeHideToolbar();
- } else {
- mDisplayView.hideToolbar();
- }
- onInverseToggled(savedInstanceState.getBoolean(KEY_INVERSE_MODE));
- // TODO: We're currently not saving and restoring scroll position.
- // We probably should. Details may require care to deal with:
- // - new display size
- // - slow recomputation if we've scrolled far.
- }
-
- private void restoreDisplay() {
- onModeChanged(mEvaluator.getDegreeMode(Evaluator.MAIN_INDEX));
- if (mCurrentState != CalculatorState.RESULT
- && mCurrentState != CalculatorState.INIT_FOR_RESULT) {
- redisplayFormula();
- }
- if (mCurrentState == CalculatorState.INPUT) {
- // This resultText will explicitly call evaluateAndNotify when ready.
- mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_EVALUATE, this);
- } else {
- // Just reevaluate.
- setState(mapFromSaved(mCurrentState));
- // Request evaluation when we know display width.
- mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_REQUIRE, this);
- }
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setContentView(R.layout.activity_calculator_main);
- setActionBar((Toolbar) findViewById(R.id.toolbar));
-
- // Hide all default options in the ActionBar.
- getActionBar().setDisplayOptions(0);
-
- // Ensure the toolbar stays visible while the options menu is displayed.
- getActionBar().addOnMenuVisibilityListener(new ActionBar.OnMenuVisibilityListener() {
- @Override
- public void onMenuVisibilityChanged(boolean isVisible) {
- mDisplayView.setForceToolbarVisible(isVisible);
- }
- });
-
- mMainCalculator = findViewById(R.id.main_calculator);
- mDisplayView = (CalculatorDisplay) findViewById(R.id.display);
- mModeView = (TextView) findViewById(R.id.mode);
- mFormulaText = (CalculatorFormula) findViewById(R.id.formula);
- mResultText = (CalculatorResult) findViewById(R.id.result);
- mFormulaContainer = (HorizontalScrollView) findViewById(R.id.formula_container);
- mEvaluator = Evaluator.getInstance(this);
- mEvaluator.setCallback(mEvaluatorCallback);
- mResultText.setEvaluator(mEvaluator, Evaluator.MAIN_INDEX);
- KeyMaps.setActivity(this);
-
- mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
- mDeleteButton = findViewById(R.id.del);
- mClearButton = findViewById(R.id.clr);
- final View numberPad = findViewById(R.id.pad_numeric);
- mEqualButton = numberPad.findViewById(R.id.eq);
- if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) {
- mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq);
- }
- final TextView decimalPointButton = (TextView) numberPad.findViewById(R.id.dec_point);
- decimalPointButton.setText(getDecimalSeparator());
-
- mInverseToggle = (TextView) findViewById(R.id.toggle_inv);
- mModeToggle = (TextView) findViewById(R.id.toggle_mode);
-
- mIsOneLine = mResultText.getVisibility() == View.INVISIBLE;
-
- mInvertibleButtons = new View[] {
- findViewById(R.id.fun_sin),
- findViewById(R.id.fun_cos),
- findViewById(R.id.fun_tan),
- findViewById(R.id.fun_ln),
- findViewById(R.id.fun_log),
- findViewById(R.id.op_sqrt)
- };
- mInverseButtons = new View[] {
- findViewById(R.id.fun_arcsin),
- findViewById(R.id.fun_arccos),
- findViewById(R.id.fun_arctan),
- findViewById(R.id.fun_exp),
- findViewById(R.id.fun_10pow),
- findViewById(R.id.op_sqr)
- };
-
- mDragLayout = (DragLayout) findViewById(R.id.drag_layout);
- mDragLayout.removeDragCallback(this);
- mDragLayout.addDragCallback(this);
- mDragLayout.setCloseCallback(this);
-
- mFormulaText.setOnContextMenuClickListener(mOnFormulaContextMenuClickListener);
- mFormulaText.setOnDisplayMemoryOperationsListener(mOnDisplayMemoryOperationsListener);
-
- mFormulaText.setOnTextSizeChangeListener(this);
- mFormulaText.addTextChangedListener(mFormulaTextWatcher);
- mDeleteButton.setOnLongClickListener(this);
-
- if (savedInstanceState != null) {
- restoreInstanceState(savedInstanceState);
- } else {
- mCurrentState = CalculatorState.INPUT;
- mEvaluator.clearMain();
- showAndMaybeHideToolbar();
- onInverseToggled(false);
- }
- restoreDisplay();
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- if (mDisplayView.isToolbarVisible()) {
- showAndMaybeHideToolbar();
- }
- // If HistoryFragment is showing, hide the main Calculator elements from accessibility.
- // This is because Talkback does not use visibility as a cue for RelativeLayout elements,
- // and RelativeLayout is the base class of DragLayout.
- // If we did not do this, it would be possible to traverse to main Calculator elements from
- // HistoryFragment.
- mMainCalculator.setImportantForAccessibility(
- mDragLayout.isOpen() ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
- : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
- }
-
- @Override
- protected void onSaveInstanceState(@NonNull Bundle outState) {
- mEvaluator.cancelAll(true);
- // If there's an animation in progress, cancel it first to ensure our state is up-to-date.
- if (mCurrentAnimator != null) {
- mCurrentAnimator.cancel();
- }
-
- super.onSaveInstanceState(outState);
- outState.putInt(KEY_DISPLAY_STATE, mCurrentState.ordinal());
- outState.putCharSequence(KEY_UNPROCESSED_CHARS, mUnprocessedChars);
- ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
- try (ObjectOutput out = new ObjectOutputStream(byteArrayStream)) {
- mEvaluator.saveInstanceState(out);
- } catch (IOException e) {
- // Impossible; No IO involved.
- throw new AssertionError("Impossible IO exception", e);
- }
- outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray());
- outState.putBoolean(KEY_INVERSE_MODE, mInverseToggle.isSelected());
- outState.putBoolean(KEY_SHOW_TOOLBAR, mDisplayView.isToolbarVisible());
- // We must wait for asynchronous writes to complete, since outState may contain
- // references to expressions being written.
- mEvaluator.waitForWrites();
- }
-
- // Set the state, updating delete label and display colors.
- // This restores display positions on moving to INPUT.
- // But movement/animation for moving to RESULT has already been done.
- private void setState(CalculatorState state) {
- if (mCurrentState != state) {
- if (state == CalculatorState.INPUT) {
- // We'll explicitly request evaluation from now on.
- mResultText.setShouldEvaluateResult(CalculatorResult.SHOULD_NOT_EVALUATE, null);
- restoreDisplayPositions();
- }
- mCurrentState = state;
-
- if (mCurrentState == CalculatorState.RESULT) {
- // No longer do this for ERROR; allow mistakes to be corrected.
- mDeleteButton.setVisibility(View.GONE);
- mClearButton.setVisibility(View.VISIBLE);
- } else {
- mDeleteButton.setVisibility(View.VISIBLE);
- mClearButton.setVisibility(View.GONE);
- }
-
- if (mIsOneLine) {
- if (mCurrentState == CalculatorState.RESULT
- || mCurrentState == CalculatorState.EVALUATE
- || mCurrentState == CalculatorState.ANIMATE) {
- mFormulaText.setVisibility(View.VISIBLE);
- mResultText.setVisibility(View.VISIBLE);
- } else if (mCurrentState == CalculatorState.ERROR) {
- mFormulaText.setVisibility(View.INVISIBLE);
- mResultText.setVisibility(View.VISIBLE);
- } else {
- mFormulaText.setVisibility(View.VISIBLE);
- mResultText.setVisibility(View.INVISIBLE);
- }
- }
-
- if (mCurrentState == CalculatorState.ERROR) {
- final int errorColor =
- ContextCompat.getColor(this, R.color.calculator_error_color);
- mFormulaText.setTextColor(errorColor);
- mResultText.setTextColor(errorColor);
- getWindow().setStatusBarColor(errorColor);
- } else if (mCurrentState != CalculatorState.RESULT) {
- mFormulaText.setTextColor(
- ContextCompat.getColor(this, R.color.display_formula_text_color));
- mResultText.setTextColor(
- ContextCompat.getColor(this, R.color.display_result_text_color));
- getWindow().setStatusBarColor(
- ContextCompat.getColor(this, R.color.calculator_statusbar_color));
- }
-
- invalidateOptionsMenu();
- }
- }
-
- public boolean isResultLayout() {
- // Note that ERROR has INPUT, not RESULT layout.
- return mCurrentState == CalculatorState.INIT_FOR_RESULT
- || mCurrentState == CalculatorState.RESULT;
- }
-
- public boolean isOneLine() {
- return mIsOneLine;
- }
-
- @Override
- protected void onDestroy() {
- mDragLayout.removeDragCallback(this);
- super.onDestroy();
- }
-
- /**
- * Destroy the evaluator and close the underlying database.
- */
- public void destroyEvaluator() {
- mEvaluator.destroyEvaluator();
- }
-
- @Override
- public void onActionModeStarted(ActionMode mode) {
- super.onActionModeStarted(mode);
- if (mode.getTag() == CalculatorFormula.TAG_ACTION_MODE) {
- mFormulaContainer.scrollTo(mFormulaText.getRight(), 0);
- }
- }
-
- /**
- * Stop any active ActionMode or ContextMenu for copy/paste actions.
- * Return true if there was one.
- */
- private boolean stopActionModeOrContextMenu() {
- return mResultText.stopActionModeOrContextMenu()
- || mFormulaText.stopActionModeOrContextMenu();
- }
-
- @Override
- public void onUserInteraction() {
- super.onUserInteraction();
-
- // If there's an animation in progress, end it immediately, so the user interaction can
- // be handled.
- if (mCurrentAnimator != null) {
- mCurrentAnimator.end();
- }
- }
-
- @Override
- public boolean dispatchTouchEvent(MotionEvent e) {
- if (e.getActionMasked() == MotionEvent.ACTION_DOWN) {
- stopActionModeOrContextMenu();
-
- final HistoryFragment historyFragment = getHistoryFragment();
- if (mDragLayout.isOpen() && historyFragment != null) {
- historyFragment.stopActionModeOrContextMenu();
- }
- }
- return super.dispatchTouchEvent(e);
- }
-
- @Override
- public void onBackPressed() {
- if (!stopActionModeOrContextMenu()) {
- final HistoryFragment historyFragment = getHistoryFragment();
- if (mDragLayout.isOpen() && historyFragment != null) {
- if (!historyFragment.stopActionModeOrContextMenu()) {
- removeHistoryFragment();
- }
- return;
- }
- if (mPadViewPager != null && mPadViewPager.getCurrentItem() != 0) {
- // Select the previous pad.
- mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
- } else {
- // If the user is currently looking at the first pad (or the pad is not paged),
- // allow the system to handle the Back button.
- super.onBackPressed();
- }
- }
- }
-
- @Override
- public boolean onKeyUp(int keyCode, KeyEvent event) {
- // Allow the system to handle special key codes (e.g. "BACK" or "DPAD").
- switch (keyCode) {
- case KeyEvent.KEYCODE_BACK:
- case KeyEvent.KEYCODE_ESCAPE:
- case KeyEvent.KEYCODE_DPAD_UP:
- case KeyEvent.KEYCODE_DPAD_DOWN:
- case KeyEvent.KEYCODE_DPAD_LEFT:
- case KeyEvent.KEYCODE_DPAD_RIGHT:
- return super.onKeyUp(keyCode, event);
- }
-
- // Stop the action mode or context menu if it's showing.
- stopActionModeOrContextMenu();
-
- // Always cancel unrequested in-progress evaluation of the main expression, so that
- // we don't have to worry about subsequent asynchronous completion.
- // Requested in-progress evaluations are handled below.
- cancelUnrequested();
-
- switch (keyCode) {
- case KeyEvent.KEYCODE_NUMPAD_ENTER:
- case KeyEvent.KEYCODE_ENTER:
- case KeyEvent.KEYCODE_DPAD_CENTER:
- mCurrentButton = mEqualButton;
- onEquals();
- return true;
- case KeyEvent.KEYCODE_DEL:
- mCurrentButton = mDeleteButton;
- onDelete();
- return true;
- case KeyEvent.KEYCODE_CLEAR:
- mCurrentButton = mClearButton;
- onClear();
- return true;
- default:
- cancelIfEvaluating(false);
- final int raw = event.getKeyCharacterMap().get(keyCode, event.getMetaState());
- if ((raw & KeyCharacterMap.COMBINING_ACCENT) != 0) {
- return true; // discard
- }
- // Try to discard non-printing characters and the like.
- // The user will have to explicitly delete other junk that gets past us.
- if (Character.isIdentifierIgnorable(raw) || Character.isWhitespace(raw)) {
- return true;
- }
- char c = (char) raw;
- if (c == '=') {
- mCurrentButton = mEqualButton;
- onEquals();
- } else {
- addChars(String.valueOf(c), true);
- redisplayAfterFormulaChange();
- }
- return true;
- }
- }
-
- /**
- * Invoked whenever the inverse button is toggled to update the UI.
- *
- * @param showInverse {@code true} if inverse functions should be shown
- */
- private void onInverseToggled(boolean showInverse) {
- mInverseToggle.setSelected(showInverse);
- if (showInverse) {
- mInverseToggle.setContentDescription(getString(R.string.desc_inv_on));
- for (View invertibleButton : mInvertibleButtons) {
- invertibleButton.setVisibility(View.GONE);
- }
- for (View inverseButton : mInverseButtons) {
- inverseButton.setVisibility(View.VISIBLE);
- }
- } else {
- mInverseToggle.setContentDescription(getString(R.string.desc_inv_off));
- for (View invertibleButton : mInvertibleButtons) {
- invertibleButton.setVisibility(View.VISIBLE);
- }
- for (View inverseButton : mInverseButtons) {
- inverseButton.setVisibility(View.GONE);
- }
- }
- }
-
- /**
- * Invoked whenever the deg/rad mode may have changed to update the UI. Note that the mode has
- * not necessarily actually changed where this is invoked.
- *
- * @param degreeMode {@code true} if in degree mode
- */
- private void onModeChanged(boolean degreeMode) {
- if (degreeMode) {
- mModeView.setText(R.string.mode_deg);
- mModeView.setContentDescription(getString(R.string.desc_mode_deg));
-
- mModeToggle.setText(R.string.mode_rad);
- mModeToggle.setContentDescription(getString(R.string.desc_switch_rad));
- } else {
- mModeView.setText(R.string.mode_rad);
- mModeView.setContentDescription(getString(R.string.desc_mode_rad));
-
- mModeToggle.setText(R.string.mode_deg);
- mModeToggle.setContentDescription(getString(R.string.desc_switch_deg));
- }
- }
-
- private void removeHistoryFragment() {
- final FragmentManager manager = getFragmentManager();
- if (manager != null && !manager.isDestroyed()) {
- manager.popBackStack(HistoryFragment.TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE);
- }
-
- // When HistoryFragment is hidden, the main Calculator is important for accessibility again.
- mMainCalculator.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
- }
-
- /**
- * Switch to INPUT from RESULT state in response to input of the specified button_id.
- * View.NO_ID is treated as an incomplete function id.
- */
- private void switchToInput(int button_id) {
- if (KeyMaps.isBinary(button_id) || KeyMaps.isSuffix(button_id)) {
- mEvaluator.collapse(mEvaluator.getMaxIndex() /* Most recent history entry */);
- } else {
- announceClearedForAccessibility();
- mEvaluator.clearMain();
- }
- setState(CalculatorState.INPUT);
- }
-
- // Add the given button id to input expression.
- // If appropriate, clear the expression before doing so.
- private void addKeyToExpr(int id) {
- if (mCurrentState == CalculatorState.ERROR) {
- setState(CalculatorState.INPUT);
- } else if (mCurrentState == CalculatorState.RESULT) {
- switchToInput(id);
- }
- if (!mEvaluator.append(id)) {
- // TODO: Some user visible feedback?
- }
- }
-
- /**
- * Add the given button id to input expression, assuming it was explicitly
- * typed/touched.
- * We perform slightly more aggressive correction than in pasted expressions.
- */
- private void addExplicitKeyToExpr(int id) {
- if (mCurrentState == CalculatorState.INPUT && id == R.id.op_sub) {
- mEvaluator.getExpr(Evaluator.MAIN_INDEX).removeTrailingAdditiveOperators();
- }
- addKeyToExpr(id);
- }
-
- public void evaluateInstantIfNecessary() {
- if (mCurrentState == CalculatorState.INPUT
- && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
- mEvaluator.evaluateAndNotify(Evaluator.MAIN_INDEX, this, mResultText);
- }
- }
-
- private void redisplayAfterFormulaChange() {
- // TODO: Could do this more incrementally.
- redisplayFormula();
- setState(CalculatorState.INPUT);
- mResultText.clear();
- if (haveUnprocessed()) {
- // Force reevaluation when text is deleted, even if expression is unchanged.
- mEvaluator.touch();
- } else {
- evaluateInstantIfNecessary();
- }
- }
-
- /**
- * Show the toolbar.
- * Automatically hide it again if it's not relevant to current formula.
- */
- private void showAndMaybeHideToolbar() {
- final boolean shouldBeVisible =
- mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
- mDisplayView.showToolbar(!shouldBeVisible);
- }
-
- /**
- * Display or hide the toolbar depending on calculator state.
- */
- private void showOrHideToolbar() {
- final boolean shouldBeVisible =
- mCurrentState == CalculatorState.INPUT && mEvaluator.hasTrigFuncs();
- if (shouldBeVisible) {
- mDisplayView.showToolbar(false);
- } else {
- mDisplayView.hideToolbar();
- }
- }
-
- public void onButtonClick(View view) {
- // Any animation is ended before we get here.
- mCurrentButton = view;
- stopActionModeOrContextMenu();
-
- // See onKey above for the rationale behind some of the behavior below:
- cancelUnrequested();
-
- final int id = view.getId();
- switch (id) {
- case R.id.eq:
- onEquals();
- break;
- case R.id.del:
- onDelete();
- break;
- case R.id.clr:
- onClear();
- return; // Toolbar visibility adjusted at end of animation.
- case R.id.toggle_inv:
- final boolean selected = !mInverseToggle.isSelected();
- mInverseToggle.setSelected(selected);
- onInverseToggled(selected);
- if (mCurrentState == CalculatorState.RESULT) {
- mResultText.redisplay(); // In case we cancelled reevaluation.
- }
- break;
- case R.id.toggle_mode:
- cancelIfEvaluating(false);
- final boolean mode = !mEvaluator.getDegreeMode(Evaluator.MAIN_INDEX);
- if (mCurrentState == CalculatorState.RESULT
- && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasTrigFuncs()) {
- // Capture current result evaluated in old mode.
- mEvaluator.collapse(mEvaluator.getMaxIndex());
- redisplayFormula();
- }
- // In input mode, we reinterpret already entered trig functions.
- mEvaluator.setDegreeMode(mode);
- onModeChanged(mode);
- // Show the toolbar to highlight the mode change.
- showAndMaybeHideToolbar();
- setState(CalculatorState.INPUT);
- mResultText.clear();
- if (!haveUnprocessed()) {
- evaluateInstantIfNecessary();
- }
- return;
- default:
- cancelIfEvaluating(false);
- if (haveUnprocessed()) {
- // For consistency, append as uninterpreted characters.
- // This may actually be useful for a left parenthesis.
- addChars(KeyMaps.toString(this, id), true);
- } else {
- addExplicitKeyToExpr(id);
- redisplayAfterFormulaChange();
- }
- break;
- }
- showOrHideToolbar();
- }
-
- void redisplayFormula() {
- SpannableStringBuilder formula
- = mEvaluator.getExpr(Evaluator.MAIN_INDEX).toSpannableStringBuilder(this);
- if (mUnprocessedChars != null) {
- // Add and highlight characters we couldn't process.
- formula.append(mUnprocessedChars, mUnprocessedColorSpan,
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- }
- mFormulaText.changeTextTo(formula);
- mFormulaText.setContentDescription(TextUtils.isEmpty(formula)
- ? getString(R.string.desc_formula) : null);
- }
-
- @Override
- public boolean onLongClick(View view) {
- mCurrentButton = view;
-
- if (view.getId() == R.id.del) {
- onClear();
- return true;
- }
- return false;
- }
-
- // Initial evaluation completed successfully. Initiate display.
- public void onEvaluate(long index, int initDisplayPrec, int msd, int leastDigPos,
- String truncatedWholeNumber) {
- if (index != Evaluator.MAIN_INDEX) {
- throw new AssertionError("Unexpected evaluation result index\n");
- }
-
- // Invalidate any options that may depend on the current result.
- invalidateOptionsMenu();
-
- mResultText.onEvaluate(index, initDisplayPrec, msd, leastDigPos, truncatedWholeNumber);
- if (mCurrentState != CalculatorState.INPUT) {
- // In EVALUATE, INIT, RESULT, or INIT_FOR_RESULT state.
- onResult(mCurrentState == CalculatorState.EVALUATE /* animate */,
- mCurrentState == CalculatorState.INIT_FOR_RESULT
- || mCurrentState == CalculatorState.RESULT /* previously preserved */);
- }
- }
-
- // Reset state to reflect evaluator cancellation. Invoked by evaluator.
- public void onCancelled(long index) {
- // Index is Evaluator.MAIN_INDEX. We should be in EVALUATE state.
- setState(CalculatorState.INPUT);
- mResultText.onCancelled(index);
- }
-
- // Reevaluation completed; ask result to redisplay current value.
- public void onReevaluate(long index) {
- // Index is Evaluator.MAIN_INDEX.
- mResultText.onReevaluate(index);
- }
-
- @Override
- public void onTextSizeChanged(final TextView textView, float oldSize) {
- if (mCurrentState != CalculatorState.INPUT) {
- // Only animate text changes that occur from user input.
- return;
- }
-
- // Calculate the values needed to perform the scale and translation animations,
- // maintaining the same apparent baseline for the displayed text.
- final float textScale = oldSize / textView.getTextSize();
- final float translationX = (1.0f - textScale) *
- (textView.getWidth() / 2.0f - textView.getPaddingEnd());
- final float translationY = (1.0f - textScale) *
- (textView.getHeight() / 2.0f - textView.getPaddingBottom());
-
- final AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.playTogether(
- ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f),
- ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f),
- ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f),
- ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f));
- animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
- animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
- animatorSet.start();
- }
-
- /**
- * Cancel any in-progress explicitly requested evaluations.
- * @param quiet suppress pop-up message. Explicit evaluation can change the expression
- value, and certainly changes the display, so it seems reasonable to warn.
- * @return true if there was such an evaluation
- */
- private boolean cancelIfEvaluating(boolean quiet) {
- if (mCurrentState == CalculatorState.EVALUATE) {
- mEvaluator.cancel(Evaluator.MAIN_INDEX, quiet);
- return true;
- } else {
- return false;
- }
- }
-
-
- private void cancelUnrequested() {
- if (mCurrentState == CalculatorState.INPUT) {
- mEvaluator.cancel(Evaluator.MAIN_INDEX, true);
- }
- }
-
- private boolean haveUnprocessed() {
- return mUnprocessedChars != null && !mUnprocessedChars.isEmpty();
- }
-
- private void onEquals() {
- // Ignore if in non-INPUT state, or if there are no operators.
- if (mCurrentState == CalculatorState.INPUT) {
- if (haveUnprocessed()) {
- setState(CalculatorState.EVALUATE);
- onError(Evaluator.MAIN_INDEX, R.string.error_syntax);
- } else if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasInterestingOps()) {
- setState(CalculatorState.EVALUATE);
- mEvaluator.requireResult(Evaluator.MAIN_INDEX, this, mResultText);
- }
- }
- }
-
- private void onDelete() {
- // Delete works like backspace; remove the last character or operator from the expression.
- // Note that we handle keyboard delete exactly like the delete button. For
- // example the delete button can be used to delete a character from an incomplete
- // function name typed on a physical keyboard.
- // This should be impossible in RESULT state.
- // If there is an in-progress explicit evaluation, just cancel it and return.
- if (cancelIfEvaluating(false)) return;
- setState(CalculatorState.INPUT);
- if (haveUnprocessed()) {
- mUnprocessedChars = mUnprocessedChars.substring(0, mUnprocessedChars.length() - 1);
- } else {
- mEvaluator.delete();
- }
- if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).isEmpty() && !haveUnprocessed()) {
- // Resulting formula won't be announced, since it's empty.
- announceClearedForAccessibility();
- }
- redisplayAfterFormulaChange();
- }
-
- private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
- final ViewGroupOverlay groupOverlay =
- (ViewGroupOverlay) getWindow().getDecorView().getOverlay();
-
- final Rect displayRect = new Rect();
- mDisplayView.getGlobalVisibleRect(displayRect);
-
- // Make reveal cover the display and status bar.
- final View revealView = new View(this);
- revealView.setBottom(displayRect.bottom);
- revealView.setLeft(displayRect.left);
- revealView.setRight(displayRect.right);
- revealView.setBackgroundColor(ContextCompat.getColor(this, colorRes));
- groupOverlay.add(revealView);
-
- final int[] clearLocation = new int[2];
- sourceView.getLocationInWindow(clearLocation);
- clearLocation[0] += sourceView.getWidth() / 2;
- clearLocation[1] += sourceView.getHeight() / 2;
-
- final int revealCenterX = clearLocation[0] - revealView.getLeft();
- final int revealCenterY = clearLocation[1] - revealView.getTop();
-
- final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2);
- final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2);
- final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2);
- final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2));
-
- final Animator revealAnimator =
- ViewAnimationUtils.createCircularReveal(revealView,
- revealCenterX, revealCenterY, 0.0f, revealRadius);
- revealAnimator.setDuration(
- getResources().getInteger(android.R.integer.config_longAnimTime));
- revealAnimator.addListener(listener);
-
- final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
- alphaAnimator.setDuration(
- getResources().getInteger(android.R.integer.config_mediumAnimTime));
-
- final AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.play(revealAnimator).before(alphaAnimator);
- animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
- animatorSet.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animator) {
- groupOverlay.remove(revealView);
- mCurrentAnimator = null;
- }
- });
-
- mCurrentAnimator = animatorSet;
- animatorSet.start();
- }
-
- private void announceClearedForAccessibility() {
- mResultText.announceForAccessibility(getResources().getString(R.string.cleared));
- }
-
- public void onClearAnimationEnd() {
- mUnprocessedChars = null;
- mResultText.clear();
- mEvaluator.clearMain();
- setState(CalculatorState.INPUT);
- redisplayFormula();
- }
-
- private void onClear() {
- if (mEvaluator.getExpr(Evaluator.MAIN_INDEX).isEmpty() && !haveUnprocessed()) {
- return;
- }
- cancelIfEvaluating(true);
- announceClearedForAccessibility();
- reveal(mCurrentButton, R.color.calculator_primary_color, new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- onClearAnimationEnd();
- showOrHideToolbar();
- }
- });
- }
-
- // Evaluation encountered en error. Display the error.
- @Override
- public void onError(final long index, final int errorResourceId) {
- if (index != Evaluator.MAIN_INDEX) {
- throw new AssertionError("Unexpected error source");
- }
- if (mCurrentState == CalculatorState.EVALUATE) {
- setState(CalculatorState.ANIMATE);
- mResultText.announceForAccessibility(getResources().getString(errorResourceId));
- reveal(mCurrentButton, R.color.calculator_error_color,
- new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- setState(CalculatorState.ERROR);
- mResultText.onError(index, errorResourceId);
- }
- });
- } else if (mCurrentState == CalculatorState.INIT
- || mCurrentState == CalculatorState.INIT_FOR_RESULT /* very unlikely */) {
- setState(CalculatorState.ERROR);
- mResultText.onError(index, errorResourceId);
- } else {
- mResultText.clear();
- }
- }
-
- // Animate movement of result into the top formula slot.
- // Result window now remains translated in the top slot while the result is displayed.
- // (We convert it back to formula use only when the user provides new input.)
- // Historical note: In the Lollipop version, this invisibly and instantaneously moved
- // formula and result displays back at the end of the animation. We no longer do that,
- // so that we can continue to properly support scrolling of the result.
- // We assume the result already contains the text to be expanded.
- private void onResult(boolean animate, boolean resultWasPreserved) {
- // Calculate the textSize that would be used to display the result in the formula.
- // For scrollable results just use the minimum textSize to maximize the number of digits
- // that are visible on screen.
- float textSize = mFormulaText.getMinimumTextSize();
- if (!mResultText.isScrollable()) {
- textSize = mFormulaText.getVariableTextSize(mResultText.getText().toString());
- }
-
- // Scale the result to match the calculated textSize, minimizing the jump-cut transition
- // when a result is reused in a subsequent expression.
- final float resultScale = textSize / mResultText.getTextSize();
-
- // Set the result's pivot to match its gravity.
- mResultText.setPivotX(mResultText.getWidth() - mResultText.getPaddingRight());
- mResultText.setPivotY(mResultText.getHeight() - mResultText.getPaddingBottom());
-
- // Calculate the necessary translations so the result takes the place of the formula and
- // the formula moves off the top of the screen.
- final float resultTranslationY = (mFormulaContainer.getBottom() - mResultText.getBottom())
- - (mFormulaText.getPaddingBottom() - mResultText.getPaddingBottom());
- float formulaTranslationY = -mFormulaContainer.getBottom();
- if (mIsOneLine) {
- // Position the result text.
- mResultText.setY(mResultText.getBottom());
- formulaTranslationY = -(findViewById(R.id.toolbar).getBottom()
- + mFormulaContainer.getBottom());
- }
-
- // Change the result's textColor to match the formula.
- final int formulaTextColor = mFormulaText.getCurrentTextColor();
-
- if (resultWasPreserved) {
- // Result was previously addded to history.
- mEvaluator.represerve();
- } else {
- // Add current result to history.
- mEvaluator.preserve(Evaluator.MAIN_INDEX, true);
- }
-
- if (animate) {
- mResultText.announceForAccessibility(getResources().getString(R.string.desc_eq));
- mResultText.announceForAccessibility(mResultText.getText());
- setState(CalculatorState.ANIMATE);
- final AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.playTogether(
- ObjectAnimator.ofPropertyValuesHolder(mResultText,
- PropertyValuesHolder.ofFloat(View.SCALE_X, resultScale),
- PropertyValuesHolder.ofFloat(View.SCALE_Y, resultScale),
- PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, resultTranslationY)),
- ObjectAnimator.ofArgb(mResultText, TEXT_COLOR, formulaTextColor),
- ObjectAnimator.ofFloat(mFormulaContainer, View.TRANSLATION_Y,
- formulaTranslationY));
- animatorSet.setDuration(getResources().getInteger(
- android.R.integer.config_longAnimTime));
- animatorSet.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- setState(CalculatorState.RESULT);
- mCurrentAnimator = null;
- }
- });
-
- mCurrentAnimator = animatorSet;
- animatorSet.start();
- } else /* No animation desired; get there fast when restarting */ {
- mResultText.setScaleX(resultScale);
- mResultText.setScaleY(resultScale);
- mResultText.setTranslationY(resultTranslationY);
- mResultText.setTextColor(formulaTextColor);
- mFormulaContainer.setTranslationY(formulaTranslationY);
- setState(CalculatorState.RESULT);
- }
- }
-
- // Restore positions of the formula and result displays back to their original,
- // pre-animation state.
- private void restoreDisplayPositions() {
- // Clear result.
- mResultText.setText("");
- // Reset all of the values modified during the animation.
- mResultText.setScaleX(1.0f);
- mResultText.setScaleY(1.0f);
- mResultText.setTranslationX(0.0f);
- mResultText.setTranslationY(0.0f);
- mFormulaContainer.setTranslationY(0.0f);
-
- mFormulaText.requestFocus();
- }
-
- @Override
- public void onClick(AlertDialogFragment fragment, int which) {
- if (which == DialogInterface.BUTTON_POSITIVE) {
- if (HistoryFragment.CLEAR_DIALOG_TAG.equals(fragment.getTag())) {
- // TODO: Try to preserve the current, saved, and memory expressions. How should we
- // handle expressions to which they refer?
- mEvaluator.clearEverything();
- // TODO: It's not clear what we should really do here. This is an initial hack.
- // May want to make onClearAnimationEnd() private if/when we fix this.
- onClearAnimationEnd();
- mEvaluatorCallback.onMemoryStateChanged();
- onBackPressed();
- } else if (Evaluator.TIMEOUT_DIALOG_TAG.equals(fragment.getTag())) {
- // Timeout extension request.
- mEvaluator.setLongTimeout();
- } else {
- Log.e(TAG, "Unknown AlertDialogFragment click:" + fragment.getTag());
- }
- }
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- super.onCreateOptionsMenu(menu);
-
- getMenuInflater().inflate(R.menu.activity_calculator, menu);
- return true;
- }
-
- @Override
- public boolean onPrepareOptionsMenu(Menu menu) {
- super.onPrepareOptionsMenu(menu);
-
- // Show the leading option when displaying a result.
- menu.findItem(R.id.menu_leading).setVisible(mCurrentState == CalculatorState.RESULT);
-
- // Show the fraction option when displaying a rational result.
- boolean visible = mCurrentState == CalculatorState.RESULT;
- final UnifiedReal mainResult = mEvaluator.getResult(Evaluator.MAIN_INDEX);
- // mainResult should never be null, but it happens. Check as a workaround to protect
- // against crashes until we find the root cause (b/34763650).
- visible &= mainResult != null && mainResult.exactlyDisplayable();
- menu.findItem(R.id.menu_fraction).setVisible(visible);
-
- return true;
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case R.id.menu_history:
- showHistoryFragment();
- return true;
- case R.id.menu_leading:
- displayFull();
- return true;
- case R.id.menu_fraction:
- displayFraction();
- return true;
- case R.id.menu_licenses:
- startActivity(new Intent(this, Licenses.class));
- return true;
- default:
- return super.onOptionsItemSelected(item);
- }
- }
-
- /* Begin override CloseCallback method. */
-
- @Override
- public void onClose() {
- removeHistoryFragment();
- }
-
- /* End override CloseCallback method. */
-
- /* Begin override DragCallback methods */
-
- public void onStartDraggingOpen() {
- mDisplayView.hideToolbar();
- showHistoryFragment();
- }
-
- @Override
- public void onInstanceStateRestored(boolean isOpen) {
- }
-
- @Override
- public void whileDragging(float yFraction) {
- }
-
- @Override
- public boolean shouldCaptureView(View view, int x, int y) {
- return view.getId() == R.id.history_frame
- && (mDragLayout.isMoving() || mDragLayout.isViewUnder(view, x, y));
- }
-
- @Override
- public int getDisplayHeight() {
- return mDisplayView.getMeasuredHeight();
- }
-
- /* End override DragCallback methods */
-
- /**
- * Change evaluation state to one that's friendly to the history fragment.
- * Return false if that was not easily possible.
- */
- private boolean prepareForHistory() {
- if (mCurrentState == CalculatorState.ANIMATE) {
- // End the current animation and signal that preparation has failed.
- // onUserInteraction is unreliable and onAnimationEnd() is asynchronous, so we
- // aren't guaranteed to be out of the ANIMATE state by the time prepareForHistory is
- // called.
- if (mCurrentAnimator != null) {
- mCurrentAnimator.end();
- }
- return false;
- } else if (mCurrentState == CalculatorState.EVALUATE) {
- // Cancel current evaluation
- cancelIfEvaluating(true /* quiet */ );
- setState(CalculatorState.INPUT);
- return true;
- } else if (mCurrentState == CalculatorState.INIT) {
- // Easiest to just refuse. Otherwise we can see a state change
- // while in history mode, which causes all sorts of problems.
- // TODO: Consider other alternatives. If we're just doing the decimal conversion
- // at the end of an evaluation, we could treat this as RESULT state.
- return false;
- }
- // We should be in INPUT, INIT_FOR_RESULT, RESULT, or ERROR state.
- return true;
- }
-
- private HistoryFragment getHistoryFragment() {
- final FragmentManager manager = getFragmentManager();
- if (manager == null || manager.isDestroyed()) {
- return null;
- }
- final Fragment fragment = manager.findFragmentByTag(HistoryFragment.TAG);
- return fragment == null || fragment.isRemoving() ? null : (HistoryFragment) fragment;
- }
-
- private void showHistoryFragment() {
- if (getHistoryFragment() != null) {
- // If the fragment already exists, do nothing.
- return;
- }
-
- final FragmentManager manager = getFragmentManager();
- if (manager == null || manager.isDestroyed() || !prepareForHistory()) {
- // If the history fragment can not be shown, close the draglayout.
- mDragLayout.setClosed();
- return;
- }
-
- stopActionModeOrContextMenu();
- manager.beginTransaction()
- .replace(R.id.history_frame, new HistoryFragment(), HistoryFragment.TAG)
- .setTransition(FragmentTransaction.TRANSIT_NONE)
- .addToBackStack(HistoryFragment.TAG)
- .commit();
-
- // When HistoryFragment is visible, hide all descendants of the main Calculator view.
- mMainCalculator.setImportantForAccessibility(
- View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
- // TODO: pass current scroll position of result
- }
-
- private void displayMessage(String title, String message) {
- AlertDialogFragment.showMessageDialog(this, title, message, null, null /* tag */);
- }
-
- private void displayFraction() {
- UnifiedReal result = mEvaluator.getResult(Evaluator.MAIN_INDEX);
- displayMessage(getString(R.string.menu_fraction),
- KeyMaps.translateResult(result.toNiceString()));
- }
-
- // Display full result to currently evaluated precision
- private void displayFull() {
- Resources res = getResources();
- String msg = mResultText.getFullText(true /* withSeparators */) + " ";
- if (mResultText.fullTextIsExact()) {
- msg += res.getString(R.string.exact);
- } else {
- msg += res.getString(R.string.approximate);
- }
- displayMessage(getString(R.string.menu_leading), msg);
- }
-
- /**
- * Add input characters to the end of the expression.
- * Map them to the appropriate button pushes when possible. Leftover characters
- * are added to mUnprocessedChars, which is presumed to immediately precede the newly
- * added characters.
- * @param moreChars characters to be added
- * @param explicit these characters were explicitly typed by the user, not pasted
- */
- private void addChars(String moreChars, boolean explicit) {
- if (mUnprocessedChars != null) {
- moreChars = mUnprocessedChars + moreChars;
- }
- int current = 0;
- int len = moreChars.length();
- boolean lastWasDigit = false;
- if (mCurrentState == CalculatorState.RESULT && len != 0) {
- // Clear display immediately for incomplete function name.
- switchToInput(KeyMaps.keyForChar(moreChars.charAt(current)));
- }
- char groupingSeparator = KeyMaps.translateResult(",").charAt(0);
- while (current < len) {
- char c = moreChars.charAt(current);
- if (Character.isSpaceChar(c) || c == groupingSeparator) {
- ++current;
- continue;
- }
- int k = KeyMaps.keyForChar(c);
- if (!explicit) {
- int expEnd;
- if (lastWasDigit && current !=
- (expEnd = Evaluator.exponentEnd(moreChars, current))) {
- // Process scientific notation with 'E' when pasting, in spite of ambiguity
- // with base of natural log.
- // Otherwise the 10^x key is the user's friend.
- mEvaluator.addExponent(moreChars, current, expEnd);
- current = expEnd;
- lastWasDigit = false;
- continue;
- } else {
- boolean isDigit = KeyMaps.digVal(k) != KeyMaps.NOT_DIGIT;
- if (current == 0 && (isDigit || k == R.id.dec_point)
- && mEvaluator.getExpr(Evaluator.MAIN_INDEX).hasTrailingConstant()) {
- // Refuse to concatenate pasted content to trailing constant.
- // This makes pasting of calculator results more consistent, whether or
- // not the old calculator instance is still around.
- addKeyToExpr(R.id.op_mul);
- }
- lastWasDigit = (isDigit || lastWasDigit && k == R.id.dec_point);
- }
- }
- if (k != View.NO_ID) {
- mCurrentButton = findViewById(k);
- if (explicit) {
- addExplicitKeyToExpr(k);
- } else {
- addKeyToExpr(k);
- }
- if (Character.isSurrogate(c)) {
- current += 2;
- } else {
- ++current;
- }
- continue;
- }
- int f = KeyMaps.funForString(moreChars, current);
- if (f != View.NO_ID) {
- mCurrentButton = findViewById(f);
- if (explicit) {
- addExplicitKeyToExpr(f);
- } else {
- addKeyToExpr(f);
- }
- if (f == R.id.op_sqrt) {
- // Square root entered as function; don't lose the parenthesis.
- addKeyToExpr(R.id.lparen);
- }
- current = moreChars.indexOf('(', current) + 1;
- continue;
- }
- // There are characters left, but we can't convert them to button presses.
- mUnprocessedChars = moreChars.substring(current);
- redisplayAfterFormulaChange();
- showOrHideToolbar();
- return;
- }
- mUnprocessedChars = null;
- redisplayAfterFormulaChange();
- showOrHideToolbar();
- }
-
- private void clearIfNotInputState() {
- if (mCurrentState == CalculatorState.ERROR
- || mCurrentState == CalculatorState.RESULT) {
- setState(CalculatorState.INPUT);
- mEvaluator.clearMain();
- }
- }
-
- /**
- * Since we only support LTR format, using the RTL comma does not make sense.
- */
- private String getDecimalSeparator() {
- final char defaultSeparator = DecimalFormatSymbols.getInstance().getDecimalSeparator();
- final char rtlComma = '\u066b';
- return defaultSeparator == rtlComma ? "," : String.valueOf(defaultSeparator);
- }
-
- /**
- * Clean up animation for context menu.
- */
- @Override
- public void onContextMenuClosed(Menu menu) {
- stopActionModeOrContextMenu();
- }
-
- public interface OnDisplayMemoryOperationsListener {
- boolean shouldDisplayMemory();
- }
-}
diff --git a/src/com/android/calculator2/CalculatorDisplay.java b/src/com/android/calculator2/CalculatorDisplay.java
deleted file mode 100644
index 341564d..0000000
--- a/src/com/android/calculator2/CalculatorDisplay.java
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.calculator2;
-
-import android.content.Context;
-import android.transition.Fade;
-import android.transition.Transition;
-import android.transition.TransitionManager;
-import android.util.AttributeSet;
-import android.view.GestureDetector;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.accessibility.AccessibilityManager;
-import android.widget.LinearLayout;
-import android.widget.Toolbar;
-
-public class CalculatorDisplay extends LinearLayout
- implements AccessibilityManager.AccessibilityStateChangeListener {
-
- /**
- * The duration in milliseconds after which to hide the toolbar.
- */
- private static final long AUTO_HIDE_DELAY_MILLIS = 3000L;
-
- /**
- * The duration in milliseconds to fade in/out the toolbar.
- */
- private static final long FADE_DURATION = 200L;
-
- private final Runnable mHideToolbarRunnable = new Runnable() {
- @Override
- public void run() {
- // Remove any duplicate callbacks to hide the toolbar.
- removeCallbacks(this);
-
- // Only animate if we have been laid out at least once.
- if (isLaidOut()) {
- TransitionManager.beginDelayedTransition(CalculatorDisplay.this, mTransition);
- }
- mToolbar.setVisibility(View.INVISIBLE);
- }
- };
-
- private final AccessibilityManager mAccessibilityManager;
- private final GestureDetector mTapDetector;
-
- private Toolbar mToolbar;
- private Transition mTransition;
-
- private boolean mForceToolbarVisible;
-
- public CalculatorDisplay(Context context) {
- this(context, null /* attrs */);
- }
-
- public CalculatorDisplay(Context context, AttributeSet attrs) {
- this(context, attrs, 0 /* defStyleAttr */);
- }
-
- public CalculatorDisplay(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- mAccessibilityManager =
- (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
-
- mTapDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
- @Override
- public boolean onDown(MotionEvent e) {
- // Remove callbacks to hide the toolbar.
- removeCallbacks(mHideToolbarRunnable);
-
- return true;
- }
-
- @Override
- public boolean onSingleTapConfirmed(MotionEvent e) {
- if (mToolbar.getVisibility() != View.VISIBLE) {
- showToolbar(true);
- } else {
- hideToolbar();
- }
-
- return true;
- }
- });
-
- // Draw the children in reverse order so that the toolbar is on top.
- setChildrenDrawingOrderEnabled(true);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
-
- mToolbar = (Toolbar) findViewById(R.id.toolbar);
- mTransition = new Fade()
- .setDuration(FADE_DURATION)
- .addTarget(mToolbar);
- }
-
- @Override
- protected int getChildDrawingOrder(int childCount, int i) {
- // Reverse the normal drawing order.
- return (childCount - 1) - i;
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- mAccessibilityManager.addAccessibilityStateChangeListener(this);
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- mAccessibilityManager.removeAccessibilityStateChangeListener(this);
- }
-
- @Override
- public void onAccessibilityStateChanged(boolean enabled) {
- // Always show the toolbar whenever accessibility is enabled.
- showToolbar(true);
- }
-
- @Override
- public boolean onInterceptTouchEvent(MotionEvent event) {
- mTapDetector.onTouchEvent(event);
- return super.onInterceptTouchEvent(event);
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- return mTapDetector.onTouchEvent(event) || super.onTouchEvent(event);
- }
-
- /**
- * Returns {@code true} if the toolbar should remain visible.
- */
- public boolean getForceToolbarVisible() {
- return mForceToolbarVisible || mAccessibilityManager.isEnabled();
- }
-
- /**
- * Forces the toolbar to remain visible.
- *
- * @param forceToolbarVisible {@code true} to keep the toolbar visible
- */
- public void setForceToolbarVisible(boolean forceToolbarVisible) {
- if (mForceToolbarVisible != forceToolbarVisible) {
- mForceToolbarVisible = forceToolbarVisible;
- showToolbar(!forceToolbarVisible);
- }
- }
-
- /**
- * Shows the toolbar.
- * @param autoHide Automatically ide toolbar again after delay
- */
- public void showToolbar(boolean autoHide) {
- // Only animate if we have been laid out at least once.
- if (isLaidOut()) {
- TransitionManager.beginDelayedTransition(this, mTransition);
- }
- mToolbar.setVisibility(View.VISIBLE);
-
- // Remove callbacks to hide the toolbar.
- removeCallbacks(mHideToolbarRunnable);
-
- // Auto hide the toolbar after 3 seconds.
- if (autoHide && !getForceToolbarVisible()) {
- postDelayed(mHideToolbarRunnable, AUTO_HIDE_DELAY_MILLIS);
- }
- }
-
- /**
- * Hides the toolbar.
- */
- public void hideToolbar() {
- if (!getForceToolbarVisible()) {
- removeCallbacks(mHideToolbarRunnable);
- mHideToolbarRunnable.run();
- }
- }
-
- public boolean isToolbarVisible() {
- return mToolbar.getVisibility() == View.VISIBLE;
- }
-}
diff --git a/src/com/android/calculator2/CalculatorExpr.java b/src/com/android/calculator2/CalculatorExpr.java
deleted file mode 100644
index 75ab1c9..0000000
--- a/src/com/android/calculator2/CalculatorExpr.java
+++ /dev/null
@@ -1,1118 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.calculator2;
-
-import android.content.Context;
-import android.text.SpannableString;
-import android.text.SpannableStringBuilder;
-import android.text.Spanned;
-import android.text.style.TtsSpan;
-
-import java.io.ByteArrayOutputStream;
-import java.io.DataInput;
-import java.io.DataOutput;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.math.BigInteger;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-
-/**
- * A mathematical expression represented as a sequence of "tokens".
- * Many tokens are represented by button ids for the corresponding operator.
- * A token may also represent the result of a previously evaluated expression.
- * The add() method adds a token to the end of the expression. The delete method() removes one.
- * Clear() deletes the entire expression contents. Eval() evaluates the expression,
- * producing a UnifiedReal result.
- * Expressions are parsed only during evaluation; no explicit parse tree is maintained.
- *
- * The write() method is used to save the current expression. Note that neither UnifiedReal
- * nor the underlying CR provide a serialization facility. Thus we save all previously
- * computed values by writing out the expression that was used to compute them, and reevaluate
- * when reading it back in.
- */
-class CalculatorExpr {
- /**
- * An interface for resolving expression indices in embedded subexpressions to
- * the associated CalculatorExpr, and associating a UnifiedReal result with it.
- * All methods are thread-safe in the strong sense; they may be called asynchronously
- * at any time from any thread.
- */
- public interface ExprResolver {
- /*
- * Retrieve the expression corresponding to index.
- */
- CalculatorExpr getExpr(long index);
- /*
- * Retrieve the degree mode associated with the expression at index i.
- */
- boolean getDegreeMode(long index);
- /*
- * Retrieve the stored result for the expression at index, or return null.
- */
- UnifiedReal getResult(long index);
- /*
- * Atomically test for an existing result, and set it if there was none.
- * Return the prior result if there was one, or the new one if there was not.
- * May only be called after getExpr.
- */
- UnifiedReal putResultIfAbsent(long index, UnifiedReal result);
- }
-
- private ArrayList<Token> mExpr; // The actual representation
- // as a list of tokens. Constant
- // tokens are always nonempty.
-
- private static enum TokenKind { CONSTANT, OPERATOR, PRE_EVAL };
- private static TokenKind[] tokenKindValues = TokenKind.values();
- private final static BigInteger BIG_MILLION = BigInteger.valueOf(1000000);
- private final static BigInteger BIG_BILLION = BigInteger.valueOf(1000000000);
-
- private static abstract class Token {
- abstract TokenKind kind();
-
- /**
- * Write token as either a very small Byte containing the TokenKind,
- * followed by data needed by subclass constructor,
- * or as a byte >= 0x20 directly describing the OPERATOR token.
- */
- abstract void write(DataOutput out) throws IOException;
-
- /**
- * Return a textual representation of the token.
- * The result is suitable for either display as part od the formula or TalkBack use.
- * It may be a SpannableString that includes added TalkBack information.
- * @param context context used for converting button ids to strings
- */
- abstract CharSequence toCharSequence(Context context);
- }
-
- /**
- * Representation of an operator token
- */
- private static class Operator extends Token {
- // TODO: rename id.
- public final int id; // We use the button resource id
- Operator(int resId) {
- id = resId;
- }
- Operator(byte op) throws IOException {
- id = KeyMaps.fromByte(op);
- }
- @Override
- void write(DataOutput out) throws IOException {
- out.writeByte(KeyMaps.toByte(id));
- }
- @Override
- public CharSequence toCharSequence(Context context) {
- String desc = KeyMaps.toDescriptiveString(context, id);
- if (desc != null) {
- SpannableString result = new SpannableString(KeyMaps.toString(context, id));
- Object descSpan = new TtsSpan.TextBuilder(desc).build();
- result.setSpan(descSpan, 0, result.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- return result;
- } else {
- return KeyMaps.toString(context, id);
- }
- }
- @Override
- TokenKind kind() { return TokenKind.OPERATOR; }
- }
-
- /**
- * Representation of a (possibly incomplete) numerical constant.
- * Supports addition and removal of trailing characters; hence mutable.
- */
- private static class Constant extends Token implements Cloneable {
- private boolean mSawDecimal;
- private String mWhole; // String preceding decimal point.
- private String mFraction; // String after decimal point.
- private int mExponent; // Explicit exponent, only generated through addExponent.
- private static int SAW_DECIMAL = 0x1;
- private static int HAS_EXPONENT = 0x2;
-
- Constant() {
- mWhole = "";
- mFraction = "";
- // mSawDecimal = false;
- // mExponent = 0;
- };
-
- Constant(DataInput in) throws IOException {
- mWhole = in.readUTF();
- byte flags = in.readByte();
- if ((flags & SAW_DECIMAL) != 0) {
- mSawDecimal = true;
- mFraction = in.readUTF();
- } else {
- // mSawDecimal = false;
- mFraction = "";
- }
- if ((flags & HAS_EXPONENT) != 0) {
- mExponent = in.readInt();
- }
- }
-
- @Override
- void write(DataOutput out) throws IOException {
- byte flags = (byte)((mSawDecimal ? SAW_DECIMAL : 0)
- | (mExponent != 0 ? HAS_EXPONENT : 0));
- out.writeByte(TokenKind.CONSTANT.ordinal());
- out.writeUTF(mWhole);
- out.writeByte(flags);
- if (mSawDecimal) {
- out.writeUTF(mFraction);
- }
- if (mExponent != 0) {
- out.writeInt(mExponent);
- }
- }
-
- // Given a button press, append corresponding digit.
- // We assume id is a digit or decimal point.
- // Just return false if this was the second (or later) decimal point
- // in this constant.
- // Assumes that this constant does not have an exponent.
- public boolean add(int id) {
- if (id == R.id.dec_point) {
- if (mSawDecimal || mExponent != 0) return false;
- mSawDecimal = true;
- return true;
- }
- int val = KeyMaps.digVal(id);
- if (mExponent != 0) {
- if (Math.abs(mExponent) <= 10000) {
- if (mExponent > 0) {
- mExponent = 10 * mExponent + val;
- } else {
- mExponent = 10 * mExponent - val;
- }
- return true;
- } else { // Too large; refuse
- return false;
- }
- }
- if (mSawDecimal) {
- mFraction += val;
- } else {
- mWhole += val;
- }
- return true;
- }
-
- public void addExponent(int exp) {
- // Note that adding a 0 exponent is a no-op. That's OK.
- mExponent = exp;
- }
-
- /**
- * Undo the last add or remove last exponent digit.
- * Assumes the constant is nonempty.
- */
- public void delete() {
- if (mExponent != 0) {
- mExponent /= 10;
- // Once zero, it can only be added back with addExponent.
- } else if (!mFraction.isEmpty()) {
- mFraction = mFraction.substring(0, mFraction.length() - 1);
- } else if (mSawDecimal) {
- mSawDecimal = false;
- } else {
- mWhole = mWhole.substring(0, mWhole.length() - 1);
- }
- }
-
- public boolean isEmpty() {
- return (mSawDecimal == false && mWhole.isEmpty());
- }
-
- /**
- * Produce human-readable string representation of constant, as typed.
- * We do add digit grouping separators to the whole number, even if not typed.
- * Result is internationalized.
- */
- @Override
- public String toString() {
- String result;
- if (mExponent != 0) {
- result = mWhole;
- } else {
- result = StringUtils.addCommas(mWhole, 0, mWhole.length());
- }
- if (mSawDecimal) {
- result += '.';
- result += mFraction;
- }
- if (mExponent != 0) {
- result += "E" + mExponent;
- }
- return KeyMaps.translateResult(result);
- }
-
- /**
- * Return BoundedRational representation of constant, if well-formed.
- * Result is never null.
- */
- public BoundedRational toRational() throws SyntaxException {
- String whole = mWhole;
- if (whole.isEmpty()) {
- if (mFraction.isEmpty()) {
- // Decimal point without digits.
- throw new SyntaxException();
- } else {
- whole = "0";
- }
- }
- BigInteger num = new BigInteger(whole + mFraction);
- BigInteger den = BigInteger.TEN.pow(mFraction.length());
- if (mExponent > 0) {
- num = num.multiply(BigInteger.TEN.pow(mExponent));
- }
- if (mExponent < 0) {
- den = den.multiply(BigInteger.TEN.pow(-mExponent));
- }
- return new BoundedRational(num, den);
- }
-
- @Override
- public CharSequence toCharSequence(Context context) {
- return toString();
- }
-
- @Override
- public TokenKind kind() {
- return TokenKind.CONSTANT;
- }
-
- // Override clone to make it public
- @Override
- public Object clone() {
- Constant result = new Constant();
- result.mWhole = mWhole;
- result.mFraction = mFraction;
- result.mSawDecimal = mSawDecimal;
- result.mExponent = mExponent;
- return result;
- }
- }
-
- /**
- * The "token" class for previously evaluated subexpressions.
- * We treat previously evaluated subexpressions as tokens. These are inserted when we either
- * continue an expression after evaluating some of it, or copy an expression and paste it back
- * in.
- * This only contains enough information to allow us to display the expression in a
- * formula, or reevaluate the expression with the aid of an ExprResolver; we no longer
- * cache the result. The expression corresponding to the index can be obtained through
- * the ExprResolver, which looks it up in a subexpression database.
- * The representation includes a UnifiedReal value. In order to
- * support saving and restoring, we also include the underlying expression itself, and the
- * context (currently just degree mode) used to evaluate it. The short string representation
- * is also stored in order to avoid potentially expensive recomputation in the UI thread.
- */
- private static class PreEval extends Token {
- public final long mIndex;
- private final String mShortRep; // Not internationalized.
- PreEval(long index, String shortRep) {
- mIndex = index;
- mShortRep = shortRep;
- }
- @Override
- // This writes out only a shallow representation of the result, without
- // information about subexpressions. To write out a deep representation, we
- // find referenced subexpressions, and iteratively write those as well.
- public void write(DataOutput out) throws IOException {
- out.writeByte(TokenKind.PRE_EVAL.ordinal());
- if (mIndex > Integer.MAX_VALUE || mIndex < Integer.MIN_VALUE) {
- // This would be millions of expressions per day for the life of the device.
- throw new AssertionError("Expression index too big");
- }
- out.writeInt((int)mIndex);
- out.writeUTF(mShortRep);
- }
- PreEval(DataInput in) throws IOException {
- mIndex = in.readInt();
- mShortRep = in.readUTF();
- }
- @Override
- public CharSequence toCharSequence(Context context) {
- return KeyMaps.translateResult(mShortRep);
- }
- @Override
- public TokenKind kind() {
- return TokenKind.PRE_EVAL;
- }
- public boolean hasEllipsis() {
- return mShortRep.lastIndexOf(KeyMaps.ELLIPSIS) != -1;
- }
- }
-
- /**
- * Read token from in.
- */
- public static Token newToken(DataInput in) throws IOException {
- byte kindByte = in.readByte();
- if (kindByte < 0x20) {
- TokenKind kind = tokenKindValues[kindByte];
- switch(kind) {
- case CONSTANT:
- return new Constant(in);
- case PRE_EVAL:
- PreEval pe = new PreEval(in);
- if (pe.mIndex == -1) {
- // Database corrupted by earlier bug.
- // Return a conspicuously wrong placeholder that won't lead to a crash.
- Constant result = new Constant();
- result.add(R.id.dec_point);
- return result;
- } else {
- return pe;
- }
- default: throw new IOException("Bad save file format");
- }
- } else {
- return new Operator(kindByte);
- }
- }
-
- CalculatorExpr() {
- mExpr = new ArrayList<Token>();
- }
-
- private CalculatorExpr(ArrayList<Token> expr) {
- mExpr = expr;
- }
-
- /**
- * Construct CalculatorExpr, by reading it from in.
- */
- CalculatorExpr(DataInput in) throws IOException {
- mExpr = new ArrayList<Token>();
- int size = in.readInt();
- for (int i = 0; i < size; ++i) {
- mExpr.add(newToken(in));
- }
- }
-
- /**
- * Write this expression to out.
- */
- public void write(DataOutput out) throws IOException {
- int size = mExpr.size();
- out.writeInt(size);
- for (int i = 0; i < size; ++i) {
- mExpr.get(i).write(out);
- }
- }
-
- /**
- * Use write() above to generate a byte array containing a serialized representation of
- * this expression.
- */
- public byte[] toBytes() {
- ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
- try (DataOutputStream out = new DataOutputStream(byteArrayStream)) {
- write(out);
- } catch (IOException e) {
- // Impossible; No IO involved.
- throw new AssertionError("Impossible IO exception", e);
- }
- return byteArrayStream.toByteArray();
- }
-
- /**
- * Does this expression end with a numeric constant?
- * As opposed to an operator or preevaluated expression.
- */
- boolean hasTrailingConstant() {
- int s = mExpr.size();
- if (s == 0) {
- return false;
- }
- Token t = mExpr.get(s-1);
- return t instanceof Constant;
- }
-
- /**
- * Does this expression end with a binary operator?
- */
- boolean hasTrailingBinary() {
- int s = mExpr.size();
- if (s == 0) return false;
- Token t = mExpr.get(s-1);
- if (!(t instanceof Operator)) return false;
- Operator o = (Operator)t;
- return (KeyMaps.isBinary(o.id));
- }
-
- /**
- * Append press of button with given id to expression.
- * If the insertion would clearly result in a syntax error, either just return false
- * and do nothing, or make an adjustment to avoid the problem. We do the latter only
- * for unambiguous consecutive binary operators, in which case we delete the first
- * operator.
- */
- boolean add(int id) {
- int s = mExpr.size();
- final int d = KeyMaps.digVal(id);
- final boolean binary = KeyMaps.isBinary(id);
- Token lastTok = s == 0 ? null : mExpr.get(s-1);
- int lastOp = lastTok instanceof Operator ? ((Operator) lastTok).id : 0;
- // Quietly replace a trailing binary operator with another one, unless the second
- // operator is minus, in which case we just allow it as a unary minus.
- if (binary && !KeyMaps.isPrefix(id)) {
- if (s == 0 || lastOp == R.id.lparen || KeyMaps.isFunc(lastOp)
- || KeyMaps.isPrefix(lastOp) && lastOp != R.id.op_sub) {
- return false;
- }
- while (hasTrailingBinary()) {
- delete();
- }
- // s invalid and not used below.
- }
- final boolean isConstPiece = (d != KeyMaps.NOT_DIGIT || id == R.id.dec_point);
- if (isConstPiece) {
- // Since we treat juxtaposition as multiplication, a constant can appear anywhere.
- if (s == 0) {
- mExpr.add(new Constant());
- s++;
- } else {
- Token last = mExpr.get(s-1);
- if(!(last instanceof Constant)) {
- if (last instanceof PreEval) {
- // Add explicit multiplication to avoid confusing display.
- mExpr.add(new Operator(R.id.op_mul));
- s++;
- }
- mExpr.add(new Constant());
- s++;
- }
- }
- return ((Constant)(mExpr.get(s-1))).add(id);
- } else {
- mExpr.add(new Operator(id));
- return true;
- }
- }
-
- /**
- * Add exponent to the constant at the end of the expression.
- * Assumes there is a constant at the end of the expression.
- */
- void addExponent(int exp) {
- Token lastTok = mExpr.get(mExpr.size() - 1);
- ((Constant) lastTok).addExponent(exp);
- }
-
- /**
- * Remove trailing op_add and op_sub operators.
- */
- void removeTrailingAdditiveOperators() {
- while (true) {
- int s = mExpr.size();
- if (s == 0) {
- break;
- }
- Token lastTok = mExpr.get(s-1);
- if (!(lastTok instanceof Operator)) {
- break;
- }
- int lastOp = ((Operator) lastTok).id;
- if (lastOp != R.id.op_add && lastOp != R.id.op_sub) {
- break;
- }
- delete();
- }
- }
-
- /**
- * Append the contents of the argument expression.
- * It is assumed that the argument expression will not change, and thus its pieces can be
- * reused directly.
- */
- public void append(CalculatorExpr expr2) {
- int s = mExpr.size();
- int s2 = expr2.mExpr.size();
- // Check that we're not concatenating Constant or PreEval tokens, since the result would
- // look like a single constant, with very mysterious results for the user.
- if (s != 0 && s2 != 0) {
- Token last = mExpr.get(s-1);
- Token first = expr2.mExpr.get(0);
- if (!(first instanceof Operator) && !(last instanceof Operator)) {
- // Fudge it by adding an explicit multiplication. We would have interpreted it as
- // such anyway, and this makes it recognizable to the user.
- mExpr.add(new Operator(R.id.op_mul));
- }
- }
- for (int i = 0; i < s2; ++i) {
- mExpr.add(expr2.mExpr.get(i));
- }
- }
-
- /**
- * Undo the last key addition, if any.
- * Or possibly remove a trailing exponent digit.
- */
- public void delete() {
- final int s = mExpr.size();
- if (s == 0) {
- return;
- }
- Token last = mExpr.get(s-1);
- if (last instanceof Constant) {
- Constant c = (Constant)last;
- c.delete();
- if (!c.isEmpty()) {
- return;
- }
- }
- mExpr.remove(s-1);
- }
-
- /**
- * Remove all tokens from the expression.
- */
- public void clear() {
- mExpr.clear();
- }
-
- public boolean isEmpty() {
- return mExpr.isEmpty();
- }
-
- /**
- * Returns a logical deep copy of the CalculatorExpr.
- * Operator and PreEval tokens are immutable, and thus aren't really copied.
- */
- public Object clone() {
- CalculatorExpr result = new CalculatorExpr();
- for (Token t : mExpr) {
- if (t instanceof Constant) {
- result.mExpr.add((Token)(((Constant)t).clone()));
- } else {
- result.mExpr.add(t);
- }
- }
- return result;
- }
-
- // Am I just a constant?
- public boolean isConstant() {
- if (mExpr.size() != 1) {
- return false;
- }
- return mExpr.get(0) instanceof Constant;
- }
-
- /**
- * Return a new expression consisting of a single token representing the current pre-evaluated
- * expression.
- * The caller supplies the expression index and short string representation.
- * The expression must have been previously evaluated.
- */
- public CalculatorExpr abbreviate(long index, String sr) {
- CalculatorExpr result = new CalculatorExpr();
- @SuppressWarnings("unchecked")
- Token t = new PreEval(index, sr);
- result.mExpr.add(t);
- return result;
- }
-
- /**
- * Internal evaluation functions return an EvalRet pair.
- * We compute rational (BoundedRational) results when possible, both as a performance
- * optimization, and to detect errors exactly when we can.
- */
- private static class EvalRet {
- public int pos; // Next position (expression index) to be parsed.
- public final UnifiedReal val; // Constructive Real result of evaluating subexpression.
- EvalRet(int p, UnifiedReal v) {
- pos = p;
- val = v;
- }
- }
-
- /**
- * Internal evaluation functions take an EvalContext argument.
- */
- private static class EvalContext {
- public final int mPrefixLength; // Length of prefix to evaluate. Not explicitly saved.
- public final boolean mDegreeMode;
- public final ExprResolver mExprResolver; // Reconstructed, not saved.
- // If we add any other kinds of evaluation modes, they go here.
- EvalContext(boolean degreeMode, int len, ExprResolver er) {
- mDegreeMode = degreeMode;
- mPrefixLength = len;
- mExprResolver = er;
- }
- EvalContext(DataInput in, int len, ExprResolver er) throws IOException {
- mDegreeMode = in.readBoolean();
- mPrefixLength = len;
- mExprResolver = er;
- }
- void write(DataOutput out) throws IOException {
- out.writeBoolean(mDegreeMode);
- }
- }
-
- private UnifiedReal toRadians(UnifiedReal x, EvalContext ec) {
- if (ec.mDegreeMode) {
- return x.multiply(UnifiedReal.RADIANS_PER_DEGREE);
- } else {
- return x;
- }
- }
-
- private UnifiedReal fromRadians(UnifiedReal x, EvalContext ec) {
- if (ec.mDegreeMode) {
- return x.divide(UnifiedReal.RADIANS_PER_DEGREE);
- } else {
- return x;
- }
- }
-
- // The following methods can all throw IndexOutOfBoundsException in the event of a syntax
- // error. We expect that to be caught in eval below.
-
- private boolean isOperatorUnchecked(int i, int op) {
- Token t = mExpr.get(i);
- if (!(t instanceof Operator)) {
- return false;
- }
- return ((Operator)(t)).id == op;
- }
-
- private boolean isOperator(int i, int op, EvalContext ec) {
- if (i >= ec.mPrefixLength) {
- return false;
- }
- return isOperatorUnchecked(i, op);
- }
-
- public static class SyntaxException extends Exception {
- public SyntaxException() {
- super();
- }
- public SyntaxException(String s) {
- super(s);
- }
- }
-
- // The following functions all evaluate some kind of expression starting at position i in
- // mExpr in a specified evaluation context. They return both the expression value (as
- // constructive real and, if applicable, as BoundedRational) and the position of the next token
- // that was not used as part of the evaluation.
- // This is essentially a simple recursive descent parser combined with expression evaluation.
-
- private EvalRet evalUnary(int i, EvalContext ec) throws SyntaxException {
- final Token t = mExpr.get(i);
- if (t instanceof Constant) {
- Constant c = (Constant)t;
- return new EvalRet(i+1,new UnifiedReal(c.toRational()));
- }
- if (t instanceof PreEval) {
- final long index = ((PreEval)t).mIndex;
- UnifiedReal res = ec.mExprResolver.getResult(index);
- if (res == null) {
- // We try to minimize this recursive evaluation case, but currently don't
- // completely avoid it.
- res = nestedEval(index, ec.mExprResolver);
- }
- return new EvalRet(i+1, res);
- }
- EvalRet argVal;
- switch(((Operator)(t)).id) {
- case R.id.const_pi:
- return new EvalRet(i+1, UnifiedReal.PI);
- case R.id.const_e:
- return new EvalRet(i+1, UnifiedReal.E);
- case R.id.op_sqrt:
- // Seems to have highest precedence.
- // Does not add implicit paren.
- // Does seem to accept a leading minus.
- if (isOperator(i+1, R.id.op_sub, ec)) {
- argVal = evalUnary(i+2, ec);
- return new EvalRet(argVal.pos, argVal.val.negate().sqrt());
- } else {
- argVal = evalUnary(i+1, ec);
- return new EvalRet(argVal.pos, argVal.val.sqrt());
- }
- case R.id.lparen:
- argVal = evalExpr(i+1, ec);
- if (isOperator(argVal.pos, R.id.rparen, ec)) {
- argVal.pos++;
- }
- return new EvalRet(argVal.pos, argVal.val);
- case R.id.fun_sin:
- argVal = evalExpr(i+1, ec);
- if (isOperator(argVal.pos, R.id.rparen, ec)) {
- argVal.pos++;
- }
- return new EvalRet(argVal.pos, toRadians(argVal.val, ec).sin());
- case R.id.fun_cos:
- argVal = evalExpr(i+1, ec);
- if (isOperator(argVal.pos, R.id.rparen, ec)) {
- argVal.pos++;
- }
- return new EvalRet(argVal.pos, toRadians(argVal.val,ec).cos());
- case R.id.fun_tan:
- argVal = evalExpr(i+1, ec);
- if (isOperator(argVal.pos, R.id.rparen, ec)) {
- argVal.pos++;
- }
- UnifiedReal arg = toRadians(argVal.val, ec);
- return new EvalRet(argVal.pos, arg.sin().divide(arg.cos()));
- case R.id.fun_ln:
- argVal = evalExpr(i+1, ec);
- if (isOperator(argVal.pos, R.id.rparen, ec)) {
- argVal.pos++;
- }
- return new EvalRet(argVal.pos, argVal.val.ln());
- case R.id.fun_exp:
- argVal = evalExpr(i+1, ec);
- if (isOperator(argVal.pos, R.id.rparen, ec)) {
- argVal.pos++;
- }
- return new EvalRet(argVal.pos, argVal.val.exp());
- case R.id.fun_log:
- argVal = evalExpr(i+1, ec);
- if (isOperator(argVal.pos, R.id.rparen, ec)) {
- argVal.pos++;
- }
- return new EvalRet(argVal.pos, argVal.val.ln().divide(UnifiedReal.TEN.ln()));
- case R.id.fun_arcsin:
- argVal = evalExpr(i+1, ec);
- if (isOperator(argVal.pos, R.id.rparen, ec)) {
- argVal.pos++;
- }
- return new EvalRet(argVal.pos, fromRadians(argVal.val.asin(), ec));
- case R.id.fun_arccos:
- argVal = evalExpr(i+1, ec);
- if (isOperator(argVal.pos, R.id.rparen, ec)) {
- argVal.pos++;
- }
- return new EvalRet(argVal.pos, fromRadians(argVal.val.acos(), ec));
- case R.id.fun_arctan:
- argVal = evalExpr(i+1, ec);
- if (isOperator(argVal.pos, R.id.rparen, ec)) {
- argVal.pos++;
- }
- return new EvalRet(argVal.pos, fromRadians(argVal.val.atan(),ec));
- default:
- throw new SyntaxException("Unrecognized token in expression");
- }
- }
-
- private static final UnifiedReal ONE_HUNDREDTH = new UnifiedReal(100).inverse();
-
- private EvalRet evalSuffix(int i, EvalContext ec) throws SyntaxException {
- final EvalRet tmp = evalUnary(i, ec);
- int cpos = tmp.pos;
- UnifiedReal val = tmp.val;
-
- boolean isFact;
- boolean isSquared = false;
- while ((isFact = isOperator(cpos, R.id.op_fact, ec)) ||
- (isSquared = isOperator(cpos, R.id.op_sqr, ec)) ||
- isOperator(cpos, R.id.op_pct, ec)) {
- if (isFact) {
- val = val.fact();
- } else if (isSquared) {
- val = val.multiply(val);
- } else /* percent */ {
- val = val.multiply(ONE_HUNDREDTH);
- }
- ++cpos;
- }
- return new EvalRet(cpos, val);
- }
-
- private EvalRet evalFactor(int i, EvalContext ec) throws SyntaxException {
- final EvalRet result1 = evalSuffix(i, ec);
- int cpos = result1.pos; // current position
- UnifiedReal val = result1.val; // value so far
- if (isOperator(cpos, R.id.op_pow, ec)) {
- final EvalRet exp = evalSignedFactor(cpos + 1, ec);
- cpos = exp.pos;
- val = val.pow(exp.val);
- }
- return new EvalRet(cpos, val);
- }
-
- private EvalRet evalSignedFactor(int i, EvalContext ec) throws SyntaxException {
- final boolean negative = isOperator(i, R.id.op_sub, ec);
- int cpos = negative ? i + 1 : i;
- EvalRet tmp = evalFactor(cpos, ec);
- cpos = tmp.pos;
- final UnifiedReal result = negative ? tmp.val.negate() : tmp.val;
- return new EvalRet(cpos, result);
- }
-
- private boolean canStartFactor(int i) {
- if (i >= mExpr.size()) return false;
- Token t = mExpr.get(i);
- if (!(t instanceof Operator)) return true;
- int id = ((Operator)(t)).id;
- if (KeyMaps.isBinary(id)) return false;
- switch (id) {
- case R.id.op_fact:
- case R.id.rparen:
- return false;
- default:
- return true;
- }
- }
-
- private EvalRet evalTerm(int i, EvalContext ec) throws SyntaxException {
- EvalRet tmp = evalSignedFactor(i, ec);
- boolean is_mul = false;
- boolean is_div = false;
- int cpos = tmp.pos; // Current position in expression.
- UnifiedReal val = tmp.val; // Current value.
- while ((is_mul = isOperator(cpos, R.id.op_mul, ec))
- || (is_div = isOperator(cpos, R.id.op_div, ec))
- || canStartFactor(cpos)) {
- if (is_mul || is_div) ++cpos;
- tmp = evalSignedFactor(cpos, ec);
- if (is_div) {
- val = val.divide(tmp.val);
- } else {
- val = val.multiply(tmp.val);
- }
- cpos = tmp.pos;
- is_mul = is_div = false;
- }
- return new EvalRet(cpos, val);
- }
-
- /**
- * Is the subexpression starting at pos a simple percent constant?
- * This is used to recognize exppressions like 200+10%, which we handle specially.
- * This is defined as a Constant or PreEval token, followed by a percent sign, and followed
- * by either nothing or an additive operator.
- * Note that we are intentionally far more restrictive in recognizing such expressions than
- * e.g. http://blogs.msdn.com/b/oldnewthing/archive/2008/01/10/7047497.aspx .
- * When in doubt, we fall back to the the naive interpretation of % as 1/100.
- * Note that 100+(10)% yields 100.1 while 100+10% yields 110. This may be controversial,
- * but is consistent with Google web search.
- */
- private boolean isPercent(int pos) {
- if (mExpr.size() < pos + 2 || !isOperatorUnchecked(pos + 1, R.id.op_pct)) {
- return false;
- }
- Token number = mExpr.get(pos);
- if (number instanceof Operator) {
- return false;
- }
- if (mExpr.size() == pos + 2) {
- return true;
- }
- if (!(mExpr.get(pos + 2) instanceof Operator)) {
- return false;
- }
- Operator op = (Operator) mExpr.get(pos + 2);
- return op.id == R.id.op_add || op.id == R.id.op_sub || op.id == R.id.rparen;
- }
-
- /**
- * Compute the multiplicative factor corresponding to an N% addition or subtraction.
- * @param pos position of Constant or PreEval expression token corresponding to N.
- * @param isSubtraction this is a subtraction, as opposed to addition.
- * @param ec usable evaluation contex; only length matters.
- * @return UnifiedReal value and position, which is pos + 2, i.e. after percent sign
- */
- private EvalRet getPercentFactor(int pos, boolean isSubtraction, EvalContext ec)
- throws SyntaxException {
- EvalRet tmp = evalUnary(pos, ec);
- UnifiedReal val = isSubtraction ? tmp.val.negate() : tmp.val;
- val = UnifiedReal.ONE.add(val.multiply(ONE_HUNDREDTH));
- return new EvalRet(pos + 2 /* after percent sign */, val);
- }
-
- private EvalRet evalExpr(int i, EvalContext ec) throws SyntaxException {
- EvalRet tmp = evalTerm(i, ec);
- boolean is_plus;
- int cpos = tmp.pos;
- UnifiedReal val = tmp.val;
- while ((is_plus = isOperator(cpos, R.id.op_add, ec))
- || isOperator(cpos, R.id.op_sub, ec)) {
- if (isPercent(cpos + 1)) {
- tmp = getPercentFactor(cpos + 1, !is_plus, ec);
- val = val.multiply(tmp.val);
- } else {
- tmp = evalTerm(cpos + 1, ec);
- if (is_plus) {
- val = val.add(tmp.val);
- } else {
- val = val.subtract(tmp.val);
- }
- }
- cpos = tmp.pos;
- }
- return new EvalRet(cpos, val);
- }
-
- /**
- * Return the starting position of the sequence of trailing binary operators.
- */
- private int trailingBinaryOpsStart() {
- int result = mExpr.size();
- while (result > 0) {
- Token last = mExpr.get(result - 1);
- if (!(last instanceof Operator)) break;
- Operator o = (Operator)last;
- if (!KeyMaps.isBinary(o.id)) break;
- --result;
- }
- return result;
- }
-
- /**
- * Is the current expression worth evaluating?
- */
- public boolean hasInterestingOps() {
- final int last = trailingBinaryOpsStart();
- int first = 0;
- if (last > first && isOperatorUnchecked(first, R.id.op_sub)) {
- // Leading minus is not by itself interesting.
- first++;
- }
- for (int i = first; i < last; ++i) {
- Token t1 = mExpr.get(i);
- if (t1 instanceof Operator
- || t1 instanceof PreEval && ((PreEval)t1).hasEllipsis()) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Does the expression contain trig operations?
- */
- public boolean hasTrigFuncs() {
- for (Token t : mExpr) {
- if (t instanceof Operator) {
- Operator o = (Operator)t;
- if (KeyMaps.isTrigFunc(o.id)) {
- return true;
- }
- }
- }
- return false;
- }
-
- /**
- * Add the indices of unevaluated PreEval expressions embedded in the current expression to
- * argument. This includes only directly referenced expressions e, not those indirectly
- * referenced by e. If the index was already present, it is not added. If the argument
- * contained no duplicates, the result will not either. New indices are added to the end of
- * the list.
- */
- private void addReferencedExprs(ArrayList<Long> list, ExprResolver er) {
- for (Token t : mExpr) {
- if (t instanceof PreEval) {
- Long index = ((PreEval) t).mIndex;
- if (er.getResult(index) == null && !list.contains(index)) {
- list.add(index);
- }
- }
- }
- }
-
- /**
- * Return a list of unevaluated expressions transitively referenced by the current one.
- * All expressions in the resulting list will have had er.getExpr() called on them.
- * The resulting list is ordered such that evaluating expressions in list order
- * should trigger few recursive evaluations.
- */
- public ArrayList<Long> getTransitivelyReferencedExprs(ExprResolver er) {
- // We could avoid triggering any recursive evaluations by actually building the
- // dependency graph and topologically sorting it. Note that sorting by index works
- // for positive and negative indices separately, but not their union. Currently we
- // just settle for reverse breadth-first-search order, which handles the common case
- // of simple dependency chains well.
- ArrayList<Long> list = new ArrayList<Long>();
- int scanned = 0; // We've added expressions referenced by [0, scanned) to the list
- addReferencedExprs(list, er);
- while (scanned != list.size()) {
- er.getExpr(list.get(scanned++)).addReferencedExprs(list, er);
- }
- Collections.reverse(list);
- return list;
- }
-
- /**
- * Evaluate the expression at the given index to a UnifiedReal.
- * Both saves and returns the result.
- */
- UnifiedReal nestedEval(long index, ExprResolver er) throws SyntaxException {
- CalculatorExpr nestedExpr = er.getExpr(index);
- EvalContext newEc = new EvalContext(er.getDegreeMode(index),
- nestedExpr.trailingBinaryOpsStart(), er);
- EvalRet new_res = nestedExpr.evalExpr(0, newEc);
- return er.putResultIfAbsent(index, new_res.val);
- }
-
- /**
- * Evaluate the expression excluding trailing binary operators.
- * Errors result in exceptions, most of which are unchecked. Should not be called
- * concurrently with modification of the expression. May take a very long time; avoid calling
- * from UI thread.
- *
- * @param degreeMode use degrees rather than radians
- */
- UnifiedReal eval(boolean degreeMode, ExprResolver er) throws SyntaxException
- // And unchecked exceptions thrown by UnifiedReal, CR,
- // and BoundedRational.
- {
- // First evaluate all indirectly referenced expressions in increasing index order.
- // This ensures that subsequent evaluation never encounters an embedded PreEval
- // expression that has not been previously evaluated.
- // We could do the embedded evaluations recursively, but that risks running out of
- // stack space.
- ArrayList<Long> referenced = getTransitivelyReferencedExprs(er);
- for (long index : referenced) {
- nestedEval(index, er);
- }
- try {
- // We currently never include trailing binary operators, but include other trailing
- // operators. Thus we usually, but not always, display results for prefixes of valid
- // expressions, and don't generate an error where we previously displayed an instant
- // result. This reflects the Android L design.
- int prefixLen = trailingBinaryOpsStart();
- EvalContext ec = new EvalContext(degreeMode, prefixLen, er);
- EvalRet res = evalExpr(0, ec);
- if (res.pos != prefixLen) {
- throw new SyntaxException("Failed to parse full expression");
- }
- return res.val;
- } catch (IndexOutOfBoundsException e) {
- throw new SyntaxException("Unexpected expression end");
- }
- }
-
- // Produce a string representation of the expression itself
- SpannableStringBuilder toSpannableStringBuilder(Context context) {
- SpannableStringBuilder ssb = new SpannableStringBuilder();
- for (Token t : mExpr) {
- ssb.append(t.toCharSequence(context));
- }
- return ssb;
- }
-}
diff --git a/src/com/android/calculator2/CalculatorFormula.java b/src/com/android/calculator2/CalculatorFormula.java
deleted file mode 100644
index 2911df8..0000000
--- a/src/com/android/calculator2/CalculatorFormula.java
+++ /dev/null
@@ -1,392 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.calculator2;
-
-import android.annotation.TargetApi;
-import android.content.ClipData;
-import android.content.ClipboardManager;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Rect;
-import android.os.Build;
-import android.text.Layout;
-import android.text.TextPaint;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.util.TypedValue;
-import android.view.ActionMode;
-import android.view.ContextMenu;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.widget.TextView;
-
-/**
- * TextView adapted for displaying the formula and allowing pasting.
- */
-public class CalculatorFormula extends AlignedTextView implements MenuItem.OnMenuItemClickListener,
- ClipboardManager.OnPrimaryClipChangedListener {
-
- public static final String TAG_ACTION_MODE = "ACTION_MODE";
-
- // Temporary paint for use in layout methods.
- private final TextPaint mTempPaint = new TextPaint();
-
- private final float mMaximumTextSize;
- private final float mMinimumTextSize;
- private final float mStepTextSize;
-
- private final ClipboardManager mClipboardManager;
-
- private int mWidthConstraint = -1;
- private ActionMode mActionMode;
- private ActionMode.Callback mPasteActionModeCallback;
- private ContextMenu mContextMenu;
- private OnTextSizeChangeListener mOnTextSizeChangeListener;
- private OnFormulaContextMenuClickListener mOnContextMenuClickListener;
- private Calculator.OnDisplayMemoryOperationsListener mOnDisplayMemoryOperationsListener;
-
- public CalculatorFormula(Context context) {
- this(context, null /* attrs */);
- }
-
- public CalculatorFormula(Context context, AttributeSet attrs) {
- this(context, attrs, 0 /* defStyleAttr */);
- }
-
- public CalculatorFormula(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- mClipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
-
- final TypedArray a = context.obtainStyledAttributes(
- attrs, R.styleable.CalculatorFormula, defStyleAttr, 0);
- mMaximumTextSize = a.getDimension(
- R.styleable.CalculatorFormula_maxTextSize, getTextSize());
- mMinimumTextSize = a.getDimension(
- R.styleable.CalculatorFormula_minTextSize, getTextSize());
- mStepTextSize = a.getDimension(R.styleable.CalculatorFormula_stepTextSize,
- (mMaximumTextSize - mMinimumTextSize) / 3);
- a.recycle();
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- setupActionMode();
- } else {
- setupContextMenu();
- }
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- if (!isLaidOut()) {
- // Prevent shrinking/resizing with our variable textSize.
- setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, mMaximumTextSize,
- false /* notifyListener */);
- setMinimumHeight(getLineHeight() + getCompoundPaddingBottom()
- + getCompoundPaddingTop());
- }
-
- // Ensure we are at least as big as our parent.
- final int width = MeasureSpec.getSize(widthMeasureSpec);
- if (getMinimumWidth() != width) {
- setMinimumWidth(width);
- }
-
- // Re-calculate our textSize based on new width.
- mWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
- - getPaddingLeft() - getPaddingRight();
- final float textSize = getVariableTextSize(getText());
- if (getTextSize() != textSize) {
- setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, textSize, false /* notifyListener */);
- }
-
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- mClipboardManager.addPrimaryClipChangedListener(this);
- onPrimaryClipChanged();
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- mClipboardManager.removePrimaryClipChangedListener(this);
- }
-
- @Override
- protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
- super.onTextChanged(text, start, lengthBefore, lengthAfter);
-
- setTextSize(TypedValue.COMPLEX_UNIT_PX, getVariableTextSize(text.toString()));
- }
-
- private void setTextSizeInternal(int unit, float size, boolean notifyListener) {
- final float oldTextSize = getTextSize();
- super.setTextSize(unit, size);
- if (notifyListener && mOnTextSizeChangeListener != null && getTextSize() != oldTextSize) {
- mOnTextSizeChangeListener.onTextSizeChanged(this, oldTextSize);
- }
- }
-
- @Override
- public void setTextSize(int unit, float size) {
- setTextSizeInternal(unit, size, true);
- }
-
- public float getMinimumTextSize() {
- return mMinimumTextSize;
- }
-
- public float getMaximumTextSize() {
- return mMaximumTextSize;
- }
-
- public float getVariableTextSize(CharSequence text) {
- if (mWidthConstraint < 0 || mMaximumTextSize <= mMinimumTextSize) {
- // Not measured, bail early.
- return getTextSize();
- }
-
- // Capture current paint state.
- mTempPaint.set(getPaint());
-
- // Step through increasing text sizes until the text would no longer fit.
- float lastFitTextSize = mMinimumTextSize;
- while (lastFitTextSize < mMaximumTextSize) {
- mTempPaint.setTextSize(Math.min(lastFitTextSize + mStepTextSize, mMaximumTextSize));
- if (Layout.getDesiredWidth(text, mTempPaint) > mWidthConstraint) {
- break;
- }
- lastFitTextSize = mTempPaint.getTextSize();
- }
-
- return lastFitTextSize;
- }
-
- /**
- * Functionally equivalent to setText(), but explicitly announce changes.
- * If the new text is an extension of the old one, announce the addition.
- * Otherwise, e.g. after deletion, announce the entire new text.
- */
- public void changeTextTo(CharSequence newText) {
- final CharSequence oldText = getText();
- final char separator = KeyMaps.translateResult(",").charAt(0);
- final CharSequence added = StringUtils.getExtensionIgnoring(newText, oldText, separator);
- if (added != null) {
- if (added.length() == 1) {
- // The algorithm for pronouncing a single character doesn't seem
- // to respect our hints. Don't give it the choice.
- final char c = added.charAt(0);
- final int id = KeyMaps.keyForChar(c);
- final String descr = KeyMaps.toDescriptiveString(getContext(), id);
- if (descr != null) {
- announceForAccessibility(descr);
- } else {
- announceForAccessibility(String.valueOf(c));
- }
- } else if (added.length() != 0) {
- announceForAccessibility(added);
- }
- } else {
- announceForAccessibility(newText);
- }
- setText(newText, BufferType.SPANNABLE);
- }
-
- public boolean stopActionModeOrContextMenu() {
- if (mActionMode != null) {
- mActionMode.finish();
- return true;
- }
- if (mContextMenu != null) {
- mContextMenu.close();
- return true;
- }
- return false;
- }
-
- public void setOnTextSizeChangeListener(OnTextSizeChangeListener listener) {
- mOnTextSizeChangeListener = listener;
- }
-
- public void setOnContextMenuClickListener(OnFormulaContextMenuClickListener listener) {
- mOnContextMenuClickListener = listener;
- }
-
- public void setOnDisplayMemoryOperationsListener(
- Calculator.OnDisplayMemoryOperationsListener listener) {
- mOnDisplayMemoryOperationsListener = listener;
- }
-
- /**
- * Use ActionMode for paste support on M and higher.
- */
- @TargetApi(Build.VERSION_CODES.M)
- private void setupActionMode() {
- mPasteActionModeCallback = new ActionMode.Callback2() {
-
- @Override
- public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
- if (onMenuItemClick(item)) {
- mode.finish();
- return true;
- } else {
- return false;
- }
- }
-
- @Override
- public boolean onCreateActionMode(ActionMode mode, Menu menu) {
- mode.setTag(TAG_ACTION_MODE);
- final MenuInflater inflater = mode.getMenuInflater();
- return createContextMenu(inflater, menu);
- }
-
- @Override
- public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
- return false;
- }
-
- @Override
- public void onDestroyActionMode(ActionMode mode) {
- mActionMode = null;
- }
-
- @Override
- public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
- super.onGetContentRect(mode, view, outRect);
- outRect.top += getTotalPaddingTop();
- outRect.right -= getTotalPaddingRight();
- outRect.bottom -= getTotalPaddingBottom();
- // Encourage menu positioning over the rightmost 10% of the screen.
- outRect.left = (int) (outRect.right * 0.9f);
- }
- };
- setOnLongClickListener(new View.OnLongClickListener() {
- @Override
- public boolean onLongClick(View v) {
- mActionMode = startActionMode(mPasteActionModeCallback, ActionMode.TYPE_FLOATING);
- return true;
- }
- });
- }
-
- /**
- * Use ContextMenu for paste support on L and lower.
- */
- private void setupContextMenu() {
- setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
- @Override
- public void onCreateContextMenu(ContextMenu contextMenu, View view,
- ContextMenu.ContextMenuInfo contextMenuInfo) {
- final MenuInflater inflater = new MenuInflater(getContext());
- createContextMenu(inflater, contextMenu);
- mContextMenu = contextMenu;
- for (int i = 0; i < contextMenu.size(); i++) {
- contextMenu.getItem(i).setOnMenuItemClickListener(CalculatorFormula.this);
- }
- }
- });
- setOnLongClickListener(new View.OnLongClickListener() {
- @Override
- public boolean onLongClick(View v) {
- return showContextMenu();
- }
- });
- }
-
- private boolean createContextMenu(MenuInflater inflater, Menu menu) {
- final boolean isPasteEnabled = isPasteEnabled();
- final boolean isMemoryEnabled = isMemoryEnabled();
- if (!isPasteEnabled && !isMemoryEnabled) {
- return false;
- }
-
- bringPointIntoView(length());
- inflater.inflate(R.menu.menu_formula, menu);
- final MenuItem pasteItem = menu.findItem(R.id.menu_paste);
- final MenuItem memoryRecallItem = menu.findItem(R.id.memory_recall);
- pasteItem.setEnabled(isPasteEnabled);
- memoryRecallItem.setEnabled(isMemoryEnabled);
- return true;
- }
-
- private void paste() {
- final ClipData primaryClip = mClipboardManager.getPrimaryClip();
- if (primaryClip != null && mOnContextMenuClickListener != null) {
- mOnContextMenuClickListener.onPaste(primaryClip);
- }
- }
-
- @Override
- public boolean onMenuItemClick(MenuItem item) {
- switch (item.getItemId()) {
- case R.id.memory_recall:
- mOnContextMenuClickListener.onMemoryRecall();
- return true;
- case R.id.menu_paste:
- paste();
- return true;
- default:
- return false;
- }
- }
-
- @Override
- public void onPrimaryClipChanged() {
- setLongClickable(isPasteEnabled() || isMemoryEnabled());
- }
-
- public void onMemoryStateChanged() {
- setLongClickable(isPasteEnabled() || isMemoryEnabled());
- }
-
- private boolean isMemoryEnabled() {
- return mOnDisplayMemoryOperationsListener != null
- && mOnDisplayMemoryOperationsListener.shouldDisplayMemory();
- }
-
- private boolean isPasteEnabled() {
- final ClipData clip = mClipboardManager.getPrimaryClip();
- if (clip == null || clip.getItemCount() == 0) {
- return false;
- }
- CharSequence clipText = null;
- try {
- clipText = clip.getItemAt(0).coerceToText(getContext());
- } catch (Exception e) {
- Log.i("Calculator", "Error reading clipboard:", e);
- }
- return !TextUtils.isEmpty(clipText);
- }
-
- public interface OnTextSizeChangeListener {
- void onTextSizeChanged(TextView textView, float oldSize);
- }
-
- public interface OnFormulaContextMenuClickListener {
- boolean onPaste(ClipData clip);
- void onMemoryRecall();
- }
-}
diff --git a/src/com/android/calculator2/CalculatorPadViewPager.java b/src/com/android/calculator2/CalculatorPadViewPager.java
deleted file mode 100644
index 0f430b0..0000000
--- a/src/com/android/calculator2/CalculatorPadViewPager.java
+++ /dev/null
@@ -1,252 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.calculator2;
-
-import android.content.Context;
-import android.graphics.Color;
-import androidx.viewpager.widget.PagerAdapter;
-import androidx.viewpager.widget.ViewPager;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.GestureDetector;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-
-public class CalculatorPadViewPager extends ViewPager {
-
- private final PagerAdapter mStaticPagerAdapter = new PagerAdapter() {
- @Override
- public int getCount() {
- return getChildCount();
- }
-
- @Override
- public View instantiateItem(ViewGroup container, final int position) {
- final View child = getChildAt(position);
-
- // Set a OnClickListener to scroll to item's position when it isn't the current item.
- child.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- setCurrentItem(position, true /* smoothScroll */);
- }
- });
- // Set an OnTouchListener to always return true for onTouch events so that a touch
- // sequence cannot pass through the item to the item below.
- child.setOnTouchListener(new OnTouchListener() {
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- v.onTouchEvent(event);
- return true;
- }
- });
-
- // Set an OnHoverListener to always return true for onHover events so that focus cannot
- // pass through the item to the item below.
- child.setOnHoverListener(new OnHoverListener() {
- @Override
- public boolean onHover(View v, MotionEvent event) {
- v.onHoverEvent(event);
- return true;
- }
- });
- // Make the item focusable so it can be selected via a11y.
- child.setFocusable(true);
- // Set the content description of the item which will be used by a11y to identify it.
- child.setContentDescription(getPageTitle(position));
-
- return child;
- }
-
- @Override
- public void destroyItem(ViewGroup container, int position, Object object) {
- removeViewAt(position);
- }
-
- @Override
- public boolean isViewFromObject(View view, Object object) {
- return view == object;
- }
-
- @Override
- public float getPageWidth(int position) {
- return position == 1 ? 7.0f / 9.0f : 1.0f;
- }
-
- @Override
- public CharSequence getPageTitle(int position) {
- final String[] pageDescriptions = getContext().getResources()
- .getStringArray(R.array.desc_pad_pages);
- return pageDescriptions[position];
- }
- };
-
- private final OnPageChangeListener mOnPageChangeListener = new SimpleOnPageChangeListener() {
- @Override
- public void onPageSelected(int position) {
- for (int i = getChildCount() - 1; i >= 0; --i) {
- final View child = getChildAt(i);
- // Only the "peeking" or covered page should be clickable.
- child.setClickable(i != position);
-
- // Prevent clicks and accessibility focus from going through to descendants of
- // other pages which are covered by the current page.
- if (child instanceof ViewGroup) {
- final ViewGroup childViewGroup = (ViewGroup) child;
- for (int j = childViewGroup.getChildCount() - 1; j >= 0; --j) {
- childViewGroup.getChildAt(j)
- .setImportantForAccessibility(i == position
- ? IMPORTANT_FOR_ACCESSIBILITY_AUTO
- : IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
- }
- }
- }
- }
- };
-
- private final PageTransformer mPageTransformer = new PageTransformer() {
- @Override
- public void transformPage(View view, float position) {
- if (position < 0.0f) {
- // Pin the left page to the left side.
- view.setTranslationX(getWidth() * -position);
- view.setAlpha(Math.max(1.0f + position, 0.0f));
- } else {
- // Use the default slide transition when moving to the next page.
- view.setTranslationX(0.0f);
- view.setAlpha(1.0f);
- }
- }
- };
-
- private final GestureDetector.SimpleOnGestureListener mGestureWatcher =
- new GestureDetector.SimpleOnGestureListener() {
- @Override
- public boolean onDown(MotionEvent e) {
- // Return true so calls to onSingleTapUp are not blocked.
- return true;
- }
-
- @Override
- public boolean onSingleTapUp(MotionEvent ev) {
- if (mClickedItemIndex != -1) {
- getChildAt(mClickedItemIndex).performClick();
- mClickedItemIndex = -1;
- return true;
- }
- return super.onSingleTapUp(ev);
- }
- };
-
- private final GestureDetector mGestureDetector;
-
- private int mClickedItemIndex = -1;
-
- public CalculatorPadViewPager(Context context) {
- this(context, null /* attrs */);
- }
-
- public CalculatorPadViewPager(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- mGestureDetector = new GestureDetector(context, mGestureWatcher);
- mGestureDetector.setIsLongpressEnabled(false);
-
- setAdapter(mStaticPagerAdapter);
- setBackgroundColor(Color.BLACK);
- setPageMargin(-getResources().getDimensionPixelSize(R.dimen.pad_page_margin));
- setPageTransformer(false, mPageTransformer);
- addOnPageChangeListener(mOnPageChangeListener);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
-
- // Invalidate the adapter's data set since children may have been added during inflation.
- getAdapter().notifyDataSetChanged();
-
- // Let page change listener know about our initial position.
- mOnPageChangeListener.onPageSelected(getCurrentItem());
- }
-
- @Override
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- try {
- // Always intercept touch events when a11y focused since otherwise they will be
- // incorrectly offset by a11y before being dispatched to children.
- if (isAccessibilityFocused() || super.onInterceptTouchEvent(ev)) {
- return true;
- }
-
- // Only allow the current item to receive touch events.
- final int action = ev.getActionMasked();
- if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) {
- // If a child is a11y focused then we must always intercept the touch event
- // since it will be incorrectly offset by a11y.
- final int childCount = getChildCount();
- for (int childIndex = childCount - 1; childIndex >= 0; --childIndex) {
- if (getChildAt(childIndex).isAccessibilityFocused()) {
- mClickedItemIndex = childIndex;
- return true;
- }
- }
-
- if (action == MotionEvent.ACTION_DOWN) {
- mClickedItemIndex = -1;
- }
-
- // Otherwise if touch is on a non-current item then intercept.
- final int actionIndex = ev.getActionIndex();
- final float x = ev.getX(actionIndex) + getScrollX();
- final float y = ev.getY(actionIndex) + getScrollY();
- for (int i = childCount - 1; i >= 0; --i) {
- final int childIndex = getChildDrawingOrder(childCount, i);
- final View child = getChildAt(childIndex);
- if (child.getVisibility() == VISIBLE
- && x >= child.getLeft() && x < child.getRight()
- && y >= child.getTop() && y < child.getBottom()) {
- if (action == MotionEvent.ACTION_DOWN) {
- mClickedItemIndex = childIndex;
- }
- return childIndex != getCurrentItem();
- }
- }
- }
-
- return false;
- } catch (IllegalArgumentException e) {
- Log.e("Calculator", "Error intercepting touch event", e);
- return false;
- }
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- try {
- // Allow both the gesture detector and super to handle the touch event so they both see
- // the full sequence of events. This should be safe since the gesture detector only
- // handle clicks and super only handles swipes.
- mGestureDetector.onTouchEvent(ev);
- return super.onTouchEvent(ev);
- } catch (IllegalArgumentException e) {
- Log.e("Calculator", "Error processing touch event", e);
- return false;
- }
- }
-}
diff --git a/src/com/android/calculator2/CalculatorResult.java b/src/com/android/calculator2/CalculatorResult.java
deleted file mode 100644
index 8fa698b..0000000
--- a/src/com/android/calculator2/CalculatorResult.java
+++ /dev/null
@@ -1,1180 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.calculator2;
-
-import android.annotation.TargetApi;
-import android.content.ClipData;
-import android.content.ClipDescription;
-import android.content.ClipboardManager;
-import android.content.Context;
-import android.graphics.Rect;
-import android.os.Build;
-import androidx.annotation.IntDef;
-import androidx.core.content.ContextCompat;
-import androidx.core.os.BuildCompat;
-import android.text.Layout;
-import android.text.Spannable;
-import android.text.SpannableString;
-import android.text.Spanned;
-import android.text.TextPaint;
-import android.text.style.BackgroundColorSpan;
-import android.text.style.ForegroundColorSpan;
-import android.text.style.RelativeSizeSpan;
-import android.util.AttributeSet;
-import android.view.ActionMode;
-import android.view.ContextMenu;
-import android.view.GestureDetector;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewConfiguration;
-import android.widget.OverScroller;
-import android.widget.Toast;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-// A text widget that is "infinitely" scrollable to the right,
-// and obtains the text to display via a callback to Logic.
-public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenuItemClickListener,
- Evaluator.EvaluationListener, Evaluator.CharMetricsInfo {
- static final int MAX_RIGHT_SCROLL = 10000000;
- static final int INVALID = MAX_RIGHT_SCROLL + 10000;
- // A larger value is unlikely to avoid running out of space
- final OverScroller mScroller;
- final GestureDetector mGestureDetector;
- private long mIndex; // Index of expression we are displaying.
- private Evaluator mEvaluator;
- private boolean mScrollable = false;
- // A scrollable result is currently displayed.
- private boolean mValid = false;
- // The result holds a valid number (not an error message).
- // A suffix of "Pos" denotes a pixel offset. Zero represents a scroll position
- // in which the decimal point is just barely visible on the right of the display.
- private int mCurrentPos;// Position of right of display relative to decimal point, in pixels.
- // Large positive values mean the decimal point is scrolled off the
- // left of the display. Zero means decimal point is barely displayed
- // on the right.
- private int mLastPos; // Position already reflected in display. Pixels.
- private int mMinPos; // Minimum position to avoid unnecessary blanks on the left. Pixels.
- private int mMaxPos; // Maximum position before we start displaying the infinite
- // sequence of trailing zeroes on the right. Pixels.
- private int mWholeLen; // Length of the whole part of current result.
- // In the following, we use a suffix of Offset to denote a character position in a numeric
- // string relative to the decimal point. Positive is to the right and negative is to
- // the left. 1 = tenths position, -1 = units. 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 index into a string representing a
- // result.
- private int mMaxCharOffset; // Character offset from decimal point of rightmost digit
- // that should be displayed, plus the length of any exponent
- // needed to display that digit.
- // Limited to MAX_RIGHT_SCROLL. Often the same as:
- private int mLsdOffset; // Position of least-significant digit in result
- private int mLastDisplayedOffset; // Offset of last digit actually displayed after adding
- // exponent.
- private boolean mWholePartFits; // Scientific notation not needed for initial display.
- private float mNoExponentCredit;
- // Fraction of digit width saved by avoiding scientific notation.
- // Only accessed from UI thread.
- private boolean mAppendExponent;
- // The result fits entirely in the display, even with an exponent,
- // but not with grouping separators. Since the result is not
- // scrollable, and we do not add the exponent to max. scroll position,
- // append an exponent insteadd of replacing trailing digits.
- private final Object mWidthLock = new Object();
- // Protects the next five fields. These fields are only
- // updated by the UI thread, and read accesses by the UI thread
- // sometimes do not acquire the lock.
- private int mWidthConstraint = 0;
- // Our total width in pixels minus space for ellipsis.
- // 0 ==> uninitialized.
- private float mCharWidth = 1;
- // Maximum character width. For now we pretend that all characters
- // have this width.
- // TODO: We're not really using a fixed width font. But it appears
- // to be close enough for the characters we use that the difference
- // is not noticeable.
- private float mGroupingSeparatorWidthRatio;
- // Fraction of digit width occupied by a digit separator.
- private float mDecimalCredit;
- // Fraction of digit width saved by replacing digit with decimal point.
- private float mNoEllipsisCredit;
- // Fraction of digit width saved by both replacing ellipsis with digit
- // and avoiding scientific notation.
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({SHOULD_REQUIRE, SHOULD_EVALUATE, SHOULD_NOT_EVALUATE})
- public @interface EvaluationRequest {}
- public static final int SHOULD_REQUIRE = 2;
- public static final int SHOULD_EVALUATE = 1;
- public static final int SHOULD_NOT_EVALUATE = 0;
- @EvaluationRequest private int mEvaluationRequest = SHOULD_REQUIRE;
- // Should we evaluate when layout completes, and how?
- private Evaluator.EvaluationListener mEvaluationListener = this;
- // Listener to use if/when evaluation is requested.
- public static final int MAX_LEADING_ZEROES = 6;
- // Maximum number of leading zeroes after decimal point before we
- // switch to scientific notation with negative exponent.
- public static final int MAX_TRAILING_ZEROES = 6;
- // Maximum number of trailing zeroes before the decimal point before
- // we switch to scientific notation with positive exponent.
- private static final int SCI_NOTATION_EXTRA = 1;
- // Extra digits for standard scientific notation. In this case we
- // have a decimal point and no ellipsis.
- // We assume that we do not drop digits to make room for the decimal
- // point in ordinary scientific notation. Thus >= 1.
- private static final int MAX_COPY_EXTRA = 100;
- // The number of extra digits we are willing to compute to copy
- // a result as an exact number.
- private static final int MAX_RECOMPUTE_DIGITS = 2000;
- // The maximum number of digits we're willing to recompute in the UI
- // thread. We only do this for known rational results, where we
- // can bound the computation cost.
- private final ForegroundColorSpan mExponentColorSpan;
- private final BackgroundColorSpan mHighlightSpan;
-
- private ActionMode mActionMode;
- private ActionMode.Callback mCopyActionModeCallback;
- private ContextMenu mContextMenu;
-
- // The user requested that the result currently being evaluated should be stored to "memory".
- private boolean mStoreToMemoryRequested = false;
-
- public CalculatorResult(Context context, AttributeSet attrs) {
- super(context, attrs);
- mScroller = new OverScroller(context);
- mHighlightSpan = new BackgroundColorSpan(getHighlightColor());
- mExponentColorSpan = new ForegroundColorSpan(
- ContextCompat.getColor(context, R.color.display_result_exponent_text_color));
- mGestureDetector = new GestureDetector(context,
- new GestureDetector.SimpleOnGestureListener() {
- @Override
- public boolean onDown(MotionEvent e) {
- return true;
- }
- @Override
- public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
- float velocityY) {
- if (!mScroller.isFinished()) {
- mCurrentPos = mScroller.getFinalX();
- }
- mScroller.forceFinished(true);
- stopActionModeOrContextMenu();
- CalculatorResult.this.cancelLongPress();
- // Ignore scrolls of error string, etc.
- if (!mScrollable) return true;
- mScroller.fling(mCurrentPos, 0, - (int) velocityX, 0 /* horizontal only */,
- mMinPos, mMaxPos, 0, 0);
- postInvalidateOnAnimation();
- return true;
- }
- @Override
- public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
- float distanceY) {
- int distance = (int)distanceX;
- if (!mScroller.isFinished()) {
- mCurrentPos = mScroller.getFinalX();
- }
- mScroller.forceFinished(true);
- stopActionModeOrContextMenu();
- CalculatorResult.this.cancelLongPress();
- if (!mScrollable) return true;
- if (mCurrentPos + distance < mMinPos) {
- distance = mMinPos - mCurrentPos;
- } else if (mCurrentPos + distance > mMaxPos) {
- distance = mMaxPos - mCurrentPos;
- }
- int duration = (int)(e2.getEventTime() - e1.getEventTime());
- if (duration < 1 || duration > 100) duration = 10;
- mScroller.startScroll(mCurrentPos, 0, distance, 0, (int)duration);
- postInvalidateOnAnimation();
- return true;
- }
- @Override
- public void onLongPress(MotionEvent e) {
- if (mValid) {
- performLongClick();
- }
- }
- });
-
- final int slop = ViewConfiguration.get(context).getScaledTouchSlop();
- setOnTouchListener(new View.OnTouchListener() {
-
- // Used to determine whether a touch event should be intercepted.
- private float mInitialDownX;
- private float mInitialDownY;
-
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- final int action = event.getActionMasked();
-
- final float x = event.getX();
- final float y = event.getY();
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- mInitialDownX = x;
- mInitialDownY = y;
- break;
- case MotionEvent.ACTION_MOVE:
- final float deltaX = Math.abs(x - mInitialDownX);
- final float deltaY = Math.abs(y - mInitialDownY);
- if (deltaX > slop && deltaX > deltaY) {
- // Prevent the DragLayout from intercepting horizontal scrolls.
- getParent().requestDisallowInterceptTouchEvent(true);
- }
- }
- return mGestureDetector.onTouchEvent(event);
- }
- });
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- setupActionMode();
- } else {
- setupContextMenu();
- }
-
- setCursorVisible(false);
- setLongClickable(false);
- setContentDescription(context.getString(R.string.desc_result));
- }
-
- void setEvaluator(Evaluator evaluator, long index) {
- mEvaluator = evaluator;
- mIndex = index;
- requestLayout();
- }
-
- // Compute maximum digit width the hard way.
- private static float getMaxDigitWidth(TextPaint paint) {
- // Compute the maximum advance width for each digit, thus accounting for between-character
- // spaces. If we ever support other kinds of digits, we may have to avoid kerning effects
- // that could reduce the advance width within this particular string.
- final String allDigits = "0123456789";
- final float[] widths = new float[allDigits.length()];
- paint.getTextWidths(allDigits, widths);
- float maxWidth = 0;
- for (float x : widths) {
- maxWidth = Math.max(x, maxWidth);
- }
- return maxWidth;
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- if (!isLaidOut()) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- // Set a minimum height so scaled error messages won't affect our layout.
- setMinimumHeight(getLineHeight() + getCompoundPaddingBottom()
- + getCompoundPaddingTop());
- }
-
- final TextPaint paint = getPaint();
- final Context context = getContext();
- final float newCharWidth = getMaxDigitWidth(paint);
- // Digits are presumed to have no more than newCharWidth.
- // There are two instances when we know that the result is otherwise narrower than
- // expected:
- // 1. For standard scientific notation (our type 1), we know that we have a norrow decimal
- // point and no (usually wide) ellipsis symbol. We allow one extra digit
- // (SCI_NOTATION_EXTRA) to compensate, and consider that in determining available width.
- // 2. If we are using digit grouping separators and a decimal point, we give ourselves
- // a fractional extra space for those separators, the value of which depends on whether
- // there is also an ellipsis.
- //
- // Maximum extra space we need in various cases:
- // Type 1 scientific notation, assuming ellipsis, minus sign and E are wider than a digit:
- // Two minus signs + "E" + "." - 3 digits.
- // Type 2 scientific notation:
- // Ellipsis + "E" + "-" - 3 digits.
- // In the absence of scientific notation, we may need a little less space.
- // We give ourselves a bit of extra credit towards comma insertion and give
- // ourselves more if we have either
- // No ellipsis, or
- // A decimal separator.
-
- // Calculate extra space we need to reserve, in addition to character count.
- final float decimalSeparatorWidth = Layout.getDesiredWidth(
- context.getString(R.string.dec_point), paint);
- final float minusWidth = Layout.getDesiredWidth(context.getString(R.string.op_sub), paint);
- final float minusExtraWidth = Math.max(minusWidth - newCharWidth, 0.0f);
- final float ellipsisWidth = Layout.getDesiredWidth(KeyMaps.ELLIPSIS, paint);
- final float ellipsisExtraWidth = Math.max(ellipsisWidth - newCharWidth, 0.0f);
- final float expWidth = Layout.getDesiredWidth(KeyMaps.translateResult("e"), paint);
- final float expExtraWidth = Math.max(expWidth - newCharWidth, 0.0f);
- final float type1Extra = 2 * minusExtraWidth + expExtraWidth + decimalSeparatorWidth;
- final float type2Extra = ellipsisExtraWidth + expExtraWidth + minusExtraWidth;
- final float extraWidth = Math.max(type1Extra, type2Extra);
- final int intExtraWidth = (int) Math.ceil(extraWidth) + 1 /* to cover rounding sins */;
- final int newWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
- - (getPaddingLeft() + getPaddingRight()) - intExtraWidth;
-
- // Calculate other width constants we need to handle grouping separators.
- final float groupingSeparatorW =
- Layout.getDesiredWidth(KeyMaps.translateResult(","), paint);
- // Credits in the absence of any scientific notation:
- float noExponentCredit = extraWidth - Math.max(ellipsisExtraWidth, minusExtraWidth);
- final float noEllipsisCredit = extraWidth - minusExtraWidth; // includes noExponentCredit.
- final float decimalCredit = Math.max(newCharWidth - decimalSeparatorWidth, 0.0f);
-
- mNoExponentCredit = noExponentCredit / newCharWidth;
- synchronized(mWidthLock) {
- mWidthConstraint = newWidthConstraint;
- mCharWidth = newCharWidth;
- mNoEllipsisCredit = noEllipsisCredit / newCharWidth;
- mDecimalCredit = decimalCredit / newCharWidth;
- mGroupingSeparatorWidthRatio = groupingSeparatorW / newCharWidth;
- }
-
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
-
- if (mEvaluator != null && mEvaluationRequest != SHOULD_NOT_EVALUATE) {
- final CalculatorExpr expr = mEvaluator.getExpr(mIndex);
- if (expr != null && expr.hasInterestingOps()) {
- if (mEvaluationRequest == SHOULD_REQUIRE) {
- mEvaluator.requireResult(mIndex, mEvaluationListener, this);
- } else {
- mEvaluator.evaluateAndNotify(mIndex, mEvaluationListener, this);
- }
- }
- }
- }
-
- /**
- * Specify whether we should evaluate result on layout.
- * @param should one of SHOULD_REQUIRE, SHOULD_EVALUATE, SHOULD_NOT_EVALUATE
- */
- public void setShouldEvaluateResult(@EvaluationRequest int request,
- Evaluator.EvaluationListener listener) {
- mEvaluationListener = listener;
- mEvaluationRequest = request;
- }
-
- // From Evaluator.CharMetricsInfo.
- @Override
- public float separatorChars(String s, int len) {
- int start = 0;
- while (start < len && !Character.isDigit(s.charAt(start))) {
- ++start;
- }
- // We assume the rest consists of digits, and for consistency with the rest
- // of the code, we assume all digits have width mCharWidth.
- final int nDigits = len - start;
- // We currently insert a digit separator every three digits.
- final int nSeparators = (nDigits - 1) / 3;
- synchronized(mWidthLock) {
- // Always return an upper bound, even in the presence of rounding errors.
- return nSeparators * mGroupingSeparatorWidthRatio;
- }
- }
-
- // From Evaluator.CharMetricsInfo.
- @Override
- public float getNoEllipsisCredit() {
- synchronized(mWidthLock) {
- return mNoEllipsisCredit;
- }
- }
-
- // From Evaluator.CharMetricsInfo.
- @Override
- public float getDecimalCredit() {
- synchronized(mWidthLock) {
- return mDecimalCredit;
- }
- }
-
- // Return the length of the exponent representation for the given exponent, in
- // characters.
- private final int expLen(int exp) {
- if (exp == 0) return 0;
- final int abs_exp_digits = (int) Math.ceil(Math.log10(Math.abs((double)exp))
- + 0.0000000001d /* Round whole numbers to next integer */);
- return abs_exp_digits + (exp >= 0 ? 1 : 2);
- }
-
- /**
- * Initiate display of a new result.
- * Only called from UI thread.
- * The parameters specify various properties of the result.
- * @param index Index of expression that was just evaluated. Currently ignored, since we only
- * expect notification for the expression result being displayed.
- * @param initPrec Initial display precision computed by evaluator. (1 = tenths digit)
- * @param msd Position of most significant digit. Offset from left of string.
- Evaluator.INVALID_MSD if unknown.
- * @param leastDigPos Position of least significant digit (1 = tenths digit)
- * or Integer.MAX_VALUE.
- * @param truncatedWholePart Result up to but not including decimal point.
- Currently we only use the length.
- */
- @Override
- public void onEvaluate(long index, int initPrec, int msd, int leastDigPos,
- String truncatedWholePart) {
- initPositions(initPrec, msd, leastDigPos, truncatedWholePart);
-
- if (mStoreToMemoryRequested) {
- mEvaluator.copyToMemory(index);
- mStoreToMemoryRequested = false;
- }
- redisplay();
- }
-
- /**
- * Store the result for this index if it is available.
- * If it is unavailable, set mStoreToMemoryRequested to indicate that we should store
- * when evaluation is complete.
- */
- public void onMemoryStore() {
- if (mEvaluator.hasResult(mIndex)) {
- mEvaluator.copyToMemory(mIndex);
- } else {
- mStoreToMemoryRequested = true;
- mEvaluator.requireResult(mIndex, this /* listener */, this /* CharMetricsInfo */);
- }
- }
-
- /**
- * Add the result to the value currently in memory.
- */
- public void onMemoryAdd() {
- mEvaluator.addToMemory(mIndex);
- }
-
- /**
- * Subtract the result from the value currently in memory.
- */
- public void onMemorySubtract() {
- mEvaluator.subtractFromMemory(mIndex);
- }
-
- /**
- * Set up scroll bounds (mMinPos, mMaxPos, etc.) and determine whether the result is
- * scrollable, based on the supplied information about the result.
- * This is unfortunately complicated because we need to predict whether trailing digits
- * will eventually be replaced by an exponent.
- * Just appending the exponent during formatting would be simpler, but would produce
- * jumpier results during transitions.
- * Only called from UI thread.
- */
- private void initPositions(int initPrecOffset, int msdIndex, int lsdOffset,
- String truncatedWholePart) {
- int maxChars = getMaxChars();
- mWholeLen = truncatedWholePart.length();
- // Allow a tiny amount of slop for associativity/rounding differences in length
- // calculation. If getPreferredPrec() decided it should fit, we want to make it fit, too.
- // We reserved one extra pixel, so the extra length is OK.
- final int nSeparatorChars = (int) Math.ceil(
- separatorChars(truncatedWholePart, truncatedWholePart.length())
- - getNoEllipsisCredit() - 0.0001f);
- mWholePartFits = mWholeLen + nSeparatorChars <= maxChars;
- mLastPos = INVALID;
- mLsdOffset = lsdOffset;
- mAppendExponent = false;
- // Prevent scrolling past initial position, which is calculated to show leading digits.
- mCurrentPos = mMinPos = (int) Math.round(initPrecOffset * mCharWidth);
- if (msdIndex == Evaluator.INVALID_MSD) {
- // Possible zero value
- if (lsdOffset == Integer.MIN_VALUE) {
- // Definite zero value.
- mMaxPos = mMinPos;
- mMaxCharOffset = (int) Math.round(mMaxPos/mCharWidth);
- mScrollable = false;
- } else {
- // May be very small nonzero value. Allow user to find out.
- mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL;
- mMinPos -= mCharWidth; // Allow for future minus sign.
- mScrollable = true;
- }
- return;
- }
- int negative = truncatedWholePart.charAt(0) == '-' ? 1 : 0;
- if (msdIndex > mWholeLen && msdIndex <= mWholeLen + 3) {
- // Avoid tiny negative exponent; pretend msdIndex is just to the right of decimal point.
- msdIndex = mWholeLen - 1;
- }
- // Set to position of leftmost significant digit relative to dec. point. Usually negative.
- int minCharOffset = msdIndex - mWholeLen;
- if (minCharOffset > -1 && minCharOffset < MAX_LEADING_ZEROES + 2) {
- // Small number of leading zeroes, avoid scientific notation.
- minCharOffset = -1;
- }
- if (lsdOffset < MAX_RIGHT_SCROLL) {
- mMaxCharOffset = lsdOffset;
- if (mMaxCharOffset < -1 && mMaxCharOffset > -(MAX_TRAILING_ZEROES + 2)) {
- mMaxCharOffset = -1;
- }
- // lsdOffset is positive or negative, never 0.
- int currentExpLen = 0; // Length of required standard scientific notation exponent.
- if (mMaxCharOffset < -1) {
- currentExpLen = expLen(-minCharOffset - 1);
- } else if (minCharOffset > -1 || mMaxCharOffset >= maxChars) {
- // Number is either entirely to the right of decimal point, or decimal point is
- // not visible when scrolled to the right.
- currentExpLen = expLen(-minCharOffset);
- }
- // Exponent length does not included added decimal point. But whenever we add a
- // decimal point, we allow an extra character (SCI_NOTATION_EXTRA).
- final int separatorLength = mWholePartFits && minCharOffset < -3 ? nSeparatorChars : 0;
- mScrollable = (mMaxCharOffset + currentExpLen + separatorLength - minCharOffset
- + negative >= maxChars);
- // Now adjust mMaxCharOffset for any required exponent.
- int newMaxCharOffset;
- if (currentExpLen > 0) {
- if (mScrollable) {
- // We'll use exponent corresponding to leastDigPos when scrolled to right.
- newMaxCharOffset = mMaxCharOffset + expLen(-lsdOffset);
- } else {
- newMaxCharOffset = mMaxCharOffset + currentExpLen;
- }
- if (mMaxCharOffset <= -1 && newMaxCharOffset > -1) {
- // Very unlikely; just drop exponent.
- mMaxCharOffset = -1;
- } else {
- mMaxCharOffset = Math.min(newMaxCharOffset, MAX_RIGHT_SCROLL);
- }
- mMaxPos = Math.min((int) Math.round(mMaxCharOffset * mCharWidth),
- MAX_RIGHT_SCROLL);
- } else if (!mWholePartFits && !mScrollable) {
- // Corner case in which entire number fits, but not with grouping separators. We
- // will use an exponent in un-scrolled position, which may hide digits. Scrolling
- // by one character will remove the exponent and reveal the last digits. Note
- // that in the forced scientific notation case, the exponent length is not
- // factored into mMaxCharOffset, since we do not want such an increase to impact
- // scrolling behavior. In the unscrollable case, we thus have to append the
- // exponent at the end using the forcePrecision argument to formatResult, in order
- // to ensure that we get the entire result.
- mScrollable = (mMaxCharOffset + expLen(-minCharOffset - 1) - minCharOffset
- + negative >= maxChars);
- if (mScrollable) {
- mMaxPos = (int) Math.ceil(mMinPos + mCharWidth);
- // Single character scroll will remove exponent and show remaining piece.
- } else {
- mMaxPos = mMinPos;
- mAppendExponent = true;
- }
- } else {
- mMaxPos = Math.min((int) Math.round(mMaxCharOffset * mCharWidth),
- MAX_RIGHT_SCROLL);
- }
- if (!mScrollable) {
- // Position the number consistently with our assumptions to make sure it
- // actually fits.
- mCurrentPos = mMaxPos;
- }
- } else {
- mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL;
- mScrollable = true;
- }
- }
-
- /**
- * Display error message indicated by resourceId.
- * UI thread only.
- */
- @Override
- public void onError(long index, int resourceId) {
- mStoreToMemoryRequested = false;
- mValid = false;
- setLongClickable(false);
- mScrollable = false;
- final String msg = getContext().getString(resourceId);
- final float measuredWidth = Layout.getDesiredWidth(msg, getPaint());
- if (measuredWidth > mWidthConstraint) {
- // Multiply by .99 to avoid rounding effects.
- final float scaleFactor = 0.99f * mWidthConstraint / measuredWidth;
- final RelativeSizeSpan smallTextSpan = new RelativeSizeSpan(scaleFactor);
- final SpannableString scaledMsg = new SpannableString(msg);
- scaledMsg.setSpan(smallTextSpan, 0, msg.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- setText(scaledMsg);
- } else {
- setText(msg);
- }
- }
-
- private final int MAX_COPY_SIZE = 1000000;
-
- /*
- * Return the most significant digit position in the given string or Evaluator.INVALID_MSD.
- * Unlike Evaluator.getMsdIndexOf, we treat a final 1 as significant.
- * Pure function; callable from anywhere.
- */
- public static int getNaiveMsdIndexOf(String s) {
- final int len = s.length();
- for (int i = 0; i < len; ++i) {
- char c = s.charAt(i);
- if (c != '-' && c != '.' && c != '0') {
- return i;
- }
- }
- return Evaluator.INVALID_MSD;
- }
-
- /**
- * Format a result returned by Evaluator.getString() into a single line containing ellipses
- * (if appropriate) and an exponent (if appropriate).
- * We add two distinct kinds of exponents:
- * (1) If the final result contains the leading digit we use standard scientific notation.
- * (2) If not, we add an exponent corresponding to an interpretation of the final result as
- * an integer.
- * We add an ellipsis on the left if the result was truncated.
- * We add ellipses and exponents in a way that leaves most digits in the position they
- * would have been in had we not done so. This minimizes jumps as a result of scrolling.
- * Result is NOT internationalized, uses "E" for exponent.
- * Called only from UI thread; We sometimes omit locking for fields.
- * @param precOffset The value that was passed to getString. Identifies the significance of
- the rightmost digit. A value of 1 means the rightmost digits corresponds to tenths.
- * @param maxDigs The maximum number of characters in the result
- * @param truncated The in parameter was already truncated, beyond possibly removing the
- minus sign.
- * @param negative The in parameter represents a negative result. (Minus sign may be removed
- without setting truncated.)
- * @param lastDisplayedOffset If not null, we set lastDisplayedOffset[0] to the offset of
- the last digit actually appearing in the display.
- * @param forcePrecision If true, we make sure that the last displayed digit corresponds to
- precOffset, and allow maxDigs to be exceeded in adding the exponent and commas.
- * @param forceSciNotation Force scientific notation. May be set because we don't have
- space for grouping separators, but whole number otherwise fits.
- * @param insertCommas Insert commas (literally, not internationalized) as digit separators.
- We only ever do this for the integral part of a number, and only when no
- exponent is displayed in the initial position. The combination of which means
- that we only do it when no exponent is displayed.
- We insert commas in a way that does consider the width of the actual localized digit
- separator. Commas count towards maxDigs as the appropriate fraction of a digit.
- */
- private String formatResult(String in, int precOffset, int maxDigs, boolean truncated,
- boolean negative, int lastDisplayedOffset[], boolean forcePrecision,
- boolean forceSciNotation, boolean insertCommas) {
- final int minusSpace = negative ? 1 : 0;
- final int msdIndex = truncated ? -1 : getNaiveMsdIndexOf(in); // INVALID_MSD is OK.
- String result = in;
- boolean needEllipsis = false;
- if (truncated || (negative && result.charAt(0) != '-')) {
- needEllipsis = true;
- result = KeyMaps.ELLIPSIS + result.substring(1, result.length());
- // Ellipsis may be removed again in the type(1) scientific notation case.
- }
- final int decIndex = result.indexOf('.');
- if (lastDisplayedOffset != null) {
- lastDisplayedOffset[0] = precOffset;
- }
- if (forceSciNotation || (decIndex == -1 || msdIndex != Evaluator.INVALID_MSD
- && msdIndex - decIndex > MAX_LEADING_ZEROES + 1) && precOffset != -1) {
- // Either:
- // 1) No decimal point displayed, and it's not just to the right of the last digit, or
- // 2) we are at the front of a number whos integral part is too large to allow
- // comma insertion, or
- // 3) we should suppress leading zeroes.
- // Add an exponent to let the user track which digits are currently displayed.
- // Start with type (2) exponent if we dropped no digits. -1 accounts for decimal point.
- // We currently never show digit separators together with an exponent.
- final int initExponent = precOffset > 0 ? -precOffset : -precOffset - 1;
- int exponent = initExponent;
- boolean hasPoint = false;
- if (!truncated && msdIndex < maxDigs - 1
- && result.length() - msdIndex + 1 + minusSpace
- <= maxDigs + SCI_NOTATION_EXTRA) {
- // Type (1) exponent computation and transformation:
- // Leading digit is in display window. Use standard calculator scientific notation
- // with one digit to the left of the decimal point. Insert decimal point and
- // delete leading zeroes.
- // We try to keep leading digits roughly in position, and never
- // lengthen the result by more than SCI_NOTATION_EXTRA.
- if (decIndex > msdIndex) {
- // In the forceSciNotation, we can have a decimal point in the relevant digit
- // range. Remove it.
- result = result.substring(0, decIndex)
- + result.substring(decIndex + 1, result.length());
- // msdIndex and precOffset unaffected.
- }
- final int resLen = result.length();
- String fraction = result.substring(msdIndex + 1, resLen);
- result = (negative ? "-" : "") + result.substring(msdIndex, msdIndex + 1)
- + "." + fraction;
- // Original exp was correct for decimal point at right of fraction.
- // Adjust by length of fraction.
- exponent = initExponent + resLen - msdIndex - 1;
- hasPoint = true;
- }
- // Exponent can't be zero.
- // Actually add the exponent of either type:
- if (!forcePrecision) {
- int dropDigits; // Digits to drop to make room for exponent.
- if (hasPoint) {
- // Type (1) exponent.
- // Drop digits even if there is room. Otherwise the scrolling gets jumpy.
- dropDigits = expLen(exponent);
- if (dropDigits >= result.length() - 1) {
- // Jumpy is better than no mantissa. Probably impossible anyway.
- dropDigits = Math.max(result.length() - 2, 0);
- }
- } else {
- // Type (2) exponent.
- // Exponent depends on the number of digits we drop, which depends on
- // exponent ...
- for (dropDigits = 2; expLen(initExponent + dropDigits) > dropDigits;
- ++dropDigits) {}
- exponent = initExponent + dropDigits;
- if (precOffset - dropDigits > mLsdOffset) {
- // This can happen if e.g. result = 10^40 + 10^10
- // It turns out we would otherwise display ...10e9 because it takes
- // the same amount of space as ...1e10 but shows one more digit.
- // But we don't want to display a trailing zero, even if it's free.
- ++dropDigits;
- ++exponent;
- }
- }
- if (dropDigits >= result.length() - 1) {
- // Display too small to show meaningful result.
- return KeyMaps.ELLIPSIS + "E" + KeyMaps.ELLIPSIS;
- }
- result = result.substring(0, result.length() - dropDigits);
- if (lastDisplayedOffset != null) {
- lastDisplayedOffset[0] -= dropDigits;
- }
- }
- result = result + "E" + Integer.toString(exponent);
- } else if (insertCommas) {
- // Add commas to the whole number section, and then truncate on left to fit,
- // counting commas as a fractional digit.
- final int wholeStart = needEllipsis ? 1 : 0;
- int orig_length = result.length();
- final float nCommaChars;
- if (decIndex != -1) {
- nCommaChars = separatorChars(result, decIndex);
- result = StringUtils.addCommas(result, wholeStart, decIndex)
- + result.substring(decIndex, orig_length);
- } else {
- nCommaChars = separatorChars(result, orig_length);
- result = StringUtils.addCommas(result, wholeStart, orig_length);
- }
- if (needEllipsis) {
- orig_length -= 1; // Exclude ellipsis.
- }
- final float len = orig_length + nCommaChars;
- int deletedChars = 0;
- final float ellipsisCredit = getNoEllipsisCredit();
- final float decimalCredit = getDecimalCredit();
- final float effectiveLen = len - (decIndex == -1 ? 0 : getDecimalCredit());
- final float ellipsisAdjustment =
- needEllipsis ? mNoExponentCredit : getNoEllipsisCredit();
- // As above, we allow for a tiny amount of extra length here, for consistency with
- // getPreferredPrec().
- if (effectiveLen - ellipsisAdjustment > (float) (maxDigs - wholeStart) + 0.0001f
- && !forcePrecision) {
- float deletedWidth = 0.0f;
- while (effectiveLen - mNoExponentCredit - deletedWidth
- > (float) (maxDigs - 1 /* for ellipsis */)) {
- if (result.charAt(deletedChars) == ',') {
- deletedWidth += mGroupingSeparatorWidthRatio;
- } else {
- deletedWidth += 1.0f;
- }
- deletedChars++;
- }
- }
- if (deletedChars > 0) {
- result = KeyMaps.ELLIPSIS + result.substring(deletedChars, result.length());
- } else if (needEllipsis) {
- result = KeyMaps.ELLIPSIS + result;
- }
- }
- return result;
- }
-
- /**
- * Get formatted, but not internationalized, result from mEvaluator.
- * @param precOffset requested position (1 = tenths) of last included digit
- * @param maxSize maximum number of characters (more or less) in result
- * @param lastDisplayedOffset zeroth entry is set to actual offset of last included digit,
- * after adjusting for exponent, etc. May be null.
- * @param forcePrecision Ensure that last included digit is at pos, at the expense
- * of treating maxSize as a soft limit.
- * @param forceSciNotation Force scientific notation, even if not required by maxSize.
- * @param insertCommas Insert commas as digit separators.
- */
- private String getFormattedResult(int precOffset, int maxSize, int lastDisplayedOffset[],
- boolean forcePrecision, boolean forceSciNotation, boolean insertCommas) {
- final boolean truncated[] = new boolean[1];
- final boolean negative[] = new boolean[1];
- final int requestedPrecOffset[] = {precOffset};
- final String rawResult = mEvaluator.getString(mIndex, requestedPrecOffset, mMaxCharOffset,
- maxSize, truncated, negative, this);
- return formatResult(rawResult, requestedPrecOffset[0], maxSize, truncated[0], negative[0],
- lastDisplayedOffset, forcePrecision, forceSciNotation, insertCommas);
- }
-
- /**
- * Return entire result (within reason) up to current displayed precision.
- * @param withSeparators Add digit separators
- */
- public String getFullText(boolean withSeparators) {
- if (!mValid) return "";
- if (!mScrollable) return getText().toString();
- return KeyMaps.translateResult(getFormattedResult(mLastDisplayedOffset, MAX_COPY_SIZE,
- null, true /* forcePrecision */, false /* forceSciNotation */, withSeparators));
- }
-
- /**
- * Did the above produce a correct result?
- * UI thread only.
- */
- public boolean fullTextIsExact() {
- return !mScrollable || (getCharOffset(mMaxPos) == getCharOffset(mCurrentPos)
- && mMaxCharOffset != MAX_RIGHT_SCROLL);
- }
-
- /**
- * Get entire result up to current displayed precision, or up to MAX_COPY_EXTRA additional
- * digits, if it will lead to an exact result.
- */
- public String getFullCopyText() {
- if (!mValid
- || mLsdOffset == Integer.MAX_VALUE
- || fullTextIsExact()
- || mWholeLen > MAX_RECOMPUTE_DIGITS
- || mWholeLen + mLsdOffset > MAX_RECOMPUTE_DIGITS
- || mLsdOffset - mLastDisplayedOffset > MAX_COPY_EXTRA) {
- return getFullText(false /* withSeparators */);
- }
- // It's reasonable to compute and copy the exact result instead.
- int fractionLsdOffset = Math.max(0, mLsdOffset);
- String rawResult = mEvaluator.getResult(mIndex).toStringTruncated(fractionLsdOffset);
- if (mLsdOffset <= -1) {
- // Result has trailing decimal point. Remove it.
- rawResult = rawResult.substring(0, rawResult.length() - 1);
- fractionLsdOffset = -1;
- }
- final String formattedResult = formatResult(rawResult, fractionLsdOffset, MAX_COPY_SIZE,
- false, rawResult.charAt(0) == '-', null, true /* forcePrecision */,
- false /* forceSciNotation */, false /* insertCommas */);
- return KeyMaps.translateResult(formattedResult);
- }
-
- /**
- * Return the maximum number of characters that will fit in the result display.
- * May be called asynchronously from non-UI thread. From Evaluator.CharMetricsInfo.
- * Returns zero if measurement hasn't completed.
- */
- @Override
- public int getMaxChars() {
- int result;
- synchronized(mWidthLock) {
- return (int) Math.floor(mWidthConstraint / mCharWidth);
- }
- }
-
- /**
- * @return {@code true} if the currently displayed result is scrollable
- */
- public boolean isScrollable() {
- return mScrollable;
- }
-
- /**
- * Map pixel position to digit offset.
- * UI thread only.
- */
- int getCharOffset(int pos) {
- return (int) Math.round(pos / mCharWidth); // Lock not needed.
- }
-
- void clear() {
- mValid = false;
- mScrollable = false;
- setText("");
- setLongClickable(false);
- }
-
- @Override
- public void onCancelled(long index) {
- clear();
- mStoreToMemoryRequested = false;
- }
-
- /**
- * Refresh display.
- * Only called in UI thread. Index argument is currently ignored.
- */
- @Override
- public void onReevaluate(long index) {
- redisplay();
- }
-
- public void redisplay() {
- int maxChars = getMaxChars();
- if (maxChars < 4) {
- // Display currently too small to display a reasonable result. Punt to avoid crash.
- return;
- }
- if (mScroller.isFinished() && length() > 0) {
- setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
- }
- int currentCharOffset = getCharOffset(mCurrentPos);
- int lastDisplayedOffset[] = new int[1];
- String result = getFormattedResult(currentCharOffset, maxChars, lastDisplayedOffset,
- mAppendExponent /* forcePrecision; preserve entire result */,
- !mWholePartFits
- && currentCharOffset == getCharOffset(mMinPos) /* forceSciNotation */,
- mWholePartFits /* insertCommas */ );
- int expIndex = result.indexOf('E');
- result = KeyMaps.translateResult(result);
- if (expIndex > 0 && result.indexOf('.') == -1) {
- // Gray out exponent if used as position indicator
- SpannableString formattedResult = new SpannableString(result);
- formattedResult.setSpan(mExponentColorSpan, expIndex, result.length(),
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- setText(formattedResult);
- } else {
- setText(result);
- }
- mLastDisplayedOffset = lastDisplayedOffset[0];
- mValid = true;
- setLongClickable(true);
- }
-
- @Override
- protected void onTextChanged(java.lang.CharSequence text, int start, int lengthBefore,
- int lengthAfter) {
- super.onTextChanged(text, start, lengthBefore, lengthAfter);
-
- if (!mScrollable || mScroller.isFinished()) {
- if (lengthBefore == 0 && lengthAfter > 0) {
- setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
- setContentDescription(null);
- } else if (lengthBefore > 0 && lengthAfter == 0) {
- setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_NONE);
- setContentDescription(getContext().getString(R.string.desc_result));
- }
- }
- }
-
- @Override
- public void computeScroll() {
- if (!mScrollable) {
- return;
- }
-
- if (mScroller.computeScrollOffset()) {
- mCurrentPos = mScroller.getCurrX();
- if (getCharOffset(mCurrentPos) != getCharOffset(mLastPos)) {
- mLastPos = mCurrentPos;
- redisplay();
- }
- }
-
- if (!mScroller.isFinished()) {
- postInvalidateOnAnimation();
- setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_NONE);
- } else if (length() > 0){
- setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
- }
- }
-
- /**
- * Use ActionMode for copy/memory support on M and higher.
- */
- @TargetApi(Build.VERSION_CODES.M)
- private void setupActionMode() {
- mCopyActionModeCallback = new ActionMode.Callback2() {
-
- @Override
- public boolean onCreateActionMode(ActionMode mode, Menu menu) {
- final MenuInflater inflater = mode.getMenuInflater();
- return createContextMenu(inflater, menu);
- }
-
- @Override
- public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
- return false; // Return false if nothing is done
- }
-
- @Override
- public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
- if (onMenuItemClick(item)) {
- mode.finish();
- return true;
- } else {
- return false;
- }
- }
-
- @Override
- public void onDestroyActionMode(ActionMode mode) {
- unhighlightResult();
- mActionMode = null;
- }
-
- @Override
- public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
- super.onGetContentRect(mode, view, outRect);
-
- outRect.left += view.getPaddingLeft();
- outRect.top += view.getPaddingTop();
- outRect.right -= view.getPaddingRight();
- outRect.bottom -= view.getPaddingBottom();
- final int width = (int) Layout.getDesiredWidth(getText(), getPaint());
- if (width < outRect.width()) {
- outRect.left = outRect.right - width;
- }
-
- if (!BuildCompat.isAtLeastN()) {
- // The CAB (prior to N) only takes the translation of a view into account, so
- // if a scale is applied to the view then the offset outRect will end up being
- // positioned incorrectly. We workaround that limitation by manually applying
- // the scale to the outRect, which the CAB will then offset to the correct
- // position.
- final float scaleX = view.getScaleX();
- final float scaleY = view.getScaleY();
- outRect.left *= scaleX;
- outRect.right *= scaleX;
- outRect.top *= scaleY;
- outRect.bottom *= scaleY;
- }
- }
- };
- setOnLongClickListener(new View.OnLongClickListener() {
- @Override
- public boolean onLongClick(View v) {
- if (mValid) {
- mActionMode = startActionMode(mCopyActionModeCallback,
- ActionMode.TYPE_FLOATING);
- return true;
- }
- return false;
- }
- });
- }
-
- /**
- * Use ContextMenu for copy/memory support on L and lower.
- */
- private void setupContextMenu() {
- setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
- @Override
- public void onCreateContextMenu(ContextMenu contextMenu, View view,
- ContextMenu.ContextMenuInfo contextMenuInfo) {
- final MenuInflater inflater = new MenuInflater(getContext());
- createContextMenu(inflater, contextMenu);
- mContextMenu = contextMenu;
- for (int i = 0; i < contextMenu.size(); i ++) {
- contextMenu.getItem(i).setOnMenuItemClickListener(CalculatorResult.this);
- }
- }
- });
- setOnLongClickListener(new View.OnLongClickListener() {
- @Override
- public boolean onLongClick(View v) {
- if (mValid) {
- return showContextMenu();
- }
- return false;
- }
- });
- }
-
- private boolean createContextMenu(MenuInflater inflater, Menu menu) {
- inflater.inflate(R.menu.menu_result, menu);
- final boolean displayMemory = mEvaluator.getMemoryIndex() != 0;
- final MenuItem memoryAddItem = menu.findItem(R.id.memory_add);
- final MenuItem memorySubtractItem = menu.findItem(R.id.memory_subtract);
- memoryAddItem.setEnabled(displayMemory);
- memorySubtractItem.setEnabled(displayMemory);
- highlightResult();
- return true;
- }
-
- public boolean stopActionModeOrContextMenu() {
- if (mActionMode != null) {
- mActionMode.finish();
- return true;
- }
- if (mContextMenu != null) {
- unhighlightResult();
- mContextMenu.close();
- return true;
- }
- return false;
- }
-
- private void highlightResult() {
- final Spannable text = (Spannable) getText();
- text.setSpan(mHighlightSpan, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- }
-
- private void unhighlightResult() {
- final Spannable text = (Spannable) getText();
- text.removeSpan(mHighlightSpan);
- }
-
- private void setPrimaryClip(ClipData clip) {
- ClipboardManager clipboard = (ClipboardManager) getContext().
- getSystemService(Context.CLIPBOARD_SERVICE);
- clipboard.setPrimaryClip(clip);
- }
-
- private void copyContent() {
- final CharSequence text = getFullCopyText();
- ClipboardManager clipboard =
- (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
- // We include a tag URI, to allow us to recognize our own results and handle them
- // specially.
- ClipData.Item newItem = new ClipData.Item(text, null, mEvaluator.capture(mIndex));
- String[] mimeTypes = new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN};
- ClipData cd = new ClipData("calculator result", mimeTypes, newItem);
- clipboard.setPrimaryClip(cd);
- Toast.makeText(getContext(), R.string.text_copied_toast, Toast.LENGTH_SHORT).show();
- }
-
- @Override
- public boolean onMenuItemClick(MenuItem item) {
- switch (item.getItemId()) {
- case R.id.memory_add:
- onMemoryAdd();
- return true;
- case R.id.memory_subtract:
- onMemorySubtract();
- return true;
- case R.id.memory_store:
- onMemoryStore();
- return true;
- case R.id.menu_copy:
- if (mEvaluator.evaluationInProgress(mIndex)) {
- // Refuse to copy placeholder characters.
- return false;
- } else {
- copyContent();
- unhighlightResult();
- return true;
- }
- default:
- return false;
- }
- }
-
- @Override
- protected void onDetachedFromWindow() {
- stopActionModeOrContextMenu();
- super.onDetachedFromWindow();
- }
-}
diff --git a/src/com/android/calculator2/CalculatorScrollView.java b/src/com/android/calculator2/CalculatorScrollView.java
deleted file mode 100644
index 018ad10..0000000
--- a/src/com/android/calculator2/CalculatorScrollView.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.calculator2;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.HorizontalScrollView;
-
-import static android.view.View.MeasureSpec.UNSPECIFIED;
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
-
-public class CalculatorScrollView extends HorizontalScrollView {
-
- public CalculatorScrollView(Context context) {
- this(context, null /* attrs */);
- }
-
- public CalculatorScrollView(Context context, AttributeSet attrs) {
- this(context, attrs, 0 /* defStyleAttr */);
- }
-
- public CalculatorScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- private static int getChildMeasureSpecCompat(int spec, int padding, int childDimension) {
- if (MeasureSpec.getMode(spec) == UNSPECIFIED
- && (childDimension == MATCH_PARENT || childDimension == WRAP_CONTENT)) {
- final int size = Math.max(0, MeasureSpec.getSize(spec) - padding);
- return MeasureSpec.makeMeasureSpec(size, UNSPECIFIED);
- }
- return ViewGroup.getChildMeasureSpec(spec, padding, childDimension);
- }
-
- @Override
- protected void measureChild(View child, int parentWidthMeasureSpec,
- int parentHeightMeasureSpec) {
- // Allow child to be as wide as they want.
- parentWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
- MeasureSpec.getSize(parentWidthMeasureSpec), UNSPECIFIED);
-
- final ViewGroup.LayoutParams lp = child.getLayoutParams();
- final int childWidthMeasureSpec = getChildMeasureSpecCompat(parentWidthMeasureSpec,
- 0 /* padding */, lp.width);
- final int childHeightMeasureSpec = getChildMeasureSpecCompat(parentHeightMeasureSpec,
- getPaddingTop() + getPaddingBottom(), lp.height);
-
- child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
- }
-
- @Override
- protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
- int parentHeightMeasureSpec, int heightUsed) {
- // Allow child to be as wide as they want.
- parentWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
- MeasureSpec.getSize(parentWidthMeasureSpec), UNSPECIFIED);
-
- final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
- final int childWidthMeasureSpec = getChildMeasureSpecCompat(parentWidthMeasureSpec,
- lp.leftMargin + lp.rightMargin, lp.width);
- final int childHeightMeasureSpec = getChildMeasureSpecCompat(parentHeightMeasureSpec,
- getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin, lp.height);
-
- child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
- }
-}
diff --git a/src/com/android/calculator2/DragController.java b/src/com/android/calculator2/DragController.java
deleted file mode 100644
index 5b9b1c6..0000000
--- a/src/com/android/calculator2/DragController.java
+++ /dev/null
@@ -1,482 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.calculator2;
-
-import android.animation.ArgbEvaluator;
-import androidx.recyclerview.widget.RecyclerView;
-import android.view.View;
-import android.widget.TextView;
-
-/**
- * Contains the logic for animating the recyclerview elements on drag.
- */
-public final class DragController {
-
- private static final String TAG = "DragController";
-
- private static final ArgbEvaluator mColorEvaluator = new ArgbEvaluator();
-
- // References to views from the Calculator Display.
- private CalculatorFormula mDisplayFormula;
- private CalculatorResult mDisplayResult;
- private View mToolbar;
-
- private int mFormulaTranslationY;
- private int mFormulaTranslationX;
- private float mFormulaScale;
- private float mResultScale;
-
- private float mResultTranslationY;
- private int mResultTranslationX;
-
- private int mDisplayHeight;
-
- private int mFormulaStartColor;
- private int mFormulaEndColor;
-
- private int mResultStartColor;
- private int mResultEndColor;
-
- // The padding at the bottom of the RecyclerView itself.
- private int mBottomPaddingHeight;
-
- private boolean mAnimationInitialized;
-
- private boolean mOneLine;
- private boolean mIsDisplayEmpty;
-
- private AnimationController mAnimationController;
-
- private Evaluator mEvaluator;
-
- public void setEvaluator(Evaluator evaluator) {
- mEvaluator = evaluator;
- }
-
- public void initializeController(boolean isResult, boolean oneLine, boolean isDisplayEmpty) {
- mOneLine = oneLine;
- mIsDisplayEmpty = isDisplayEmpty;
- if (mIsDisplayEmpty) {
- // Empty display
- mAnimationController = new EmptyAnimationController();
- } else if (isResult) {
- // Result
- mAnimationController = new ResultAnimationController();
- } else {
- // There is something in the formula field. There may or may not be
- // a quick result.
- mAnimationController = new AnimationController();
- }
- }
-
- public void setDisplayFormula(CalculatorFormula formula) {
- mDisplayFormula = formula;
- }
-
- public void setDisplayResult(CalculatorResult result) {
- mDisplayResult = result;
- }
-
- public void setToolbar(View toolbar) {
- mToolbar = toolbar;
- }
-
- public void animateViews(float yFraction, RecyclerView recyclerView) {
- if (mDisplayFormula == null
- || mDisplayResult == null
- || mToolbar == null
- || mEvaluator == null) {
- // Bail if we aren't yet initialized.
- return;
- }
-
- final HistoryAdapter.ViewHolder vh =
- (HistoryAdapter.ViewHolder) recyclerView.findViewHolderForAdapterPosition(0);
- if (yFraction > 0 && vh != null) {
- recyclerView.setVisibility(View.VISIBLE);
- }
- if (vh != null && !mIsDisplayEmpty
- && vh.getItemViewType() == HistoryAdapter.HISTORY_VIEW_TYPE) {
- final AlignedTextView formula = vh.getFormula();
- final CalculatorResult result = vh.getResult();
- final TextView date = vh.getDate();
- final View divider = vh.getDivider();
-
- if (!mAnimationInitialized) {
- mBottomPaddingHeight = recyclerView.getPaddingBottom();
-
- mAnimationController.initializeScales(formula, result);
-
- mAnimationController.initializeColorAnimators(formula, result);
-
- mAnimationController.initializeFormulaTranslationX(formula);
-
- mAnimationController.initializeFormulaTranslationY(formula, result);
-
- mAnimationController.initializeResultTranslationX(result);
-
- mAnimationController.initializeResultTranslationY(result);
-
- mAnimationInitialized = true;
- }
-
- result.setScaleX(mAnimationController.getResultScale(yFraction));
- result.setScaleY(mAnimationController.getResultScale(yFraction));
-
- formula.setScaleX(mAnimationController.getFormulaScale(yFraction));
- formula.setScaleY(mAnimationController.getFormulaScale(yFraction));
-
- formula.setPivotX(formula.getWidth() - formula.getPaddingEnd());
- formula.setPivotY(formula.getHeight() - formula.getPaddingBottom());
-
- result.setPivotX(result.getWidth() - result.getPaddingEnd());
- result.setPivotY(result.getHeight() - result.getPaddingBottom());
-
- formula.setTranslationX(mAnimationController.getFormulaTranslationX(yFraction));
- formula.setTranslationY(mAnimationController.getFormulaTranslationY(yFraction));
-
- result.setTranslationX(mAnimationController.getResultTranslationX(yFraction));
- result.setTranslationY(mAnimationController.getResultTranslationY(yFraction));
-
- formula.setTextColor((int) mColorEvaluator.evaluate(yFraction, mFormulaStartColor,
- mFormulaEndColor));
-
- result.setTextColor((int) mColorEvaluator.evaluate(yFraction, mResultStartColor,
- mResultEndColor));
-
- date.setTranslationY(mAnimationController.getDateTranslationY(yFraction));
- divider.setTranslationY(mAnimationController.getDateTranslationY(yFraction));
- } else if (mIsDisplayEmpty) {
- // There is no current expression but we still need to collect information
- // to translate the other viewholders.
- if (!mAnimationInitialized) {
- mAnimationController.initializeDisplayHeight();
- mAnimationInitialized = true;
- }
- }
-
- // Move up all ViewHolders above the current expression; if there is no current expression,
- // we're translating all the viewholders.
- for (int i = recyclerView.getChildCount() - 1;
- i >= mAnimationController.getFirstTranslatedViewHolderIndex();
- --i) {
- final RecyclerView.ViewHolder vh2 =
- recyclerView.getChildViewHolder(recyclerView.getChildAt(i));
- if (vh2 != null) {
- final View view = vh2.itemView;
- if (view != null) {
- view.setTranslationY(
- mAnimationController.getHistoryElementTranslationY(yFraction));
- }
- }
- }
- }
-
- /**
- * Reset all initialized values.
- */
- public void initializeAnimation(boolean isResult, boolean oneLine, boolean isDisplayEmpty) {
- mAnimationInitialized = false;
- initializeController(isResult, oneLine, isDisplayEmpty);
- }
-
- public interface AnimateTextInterface {
-
- void initializeDisplayHeight();
-
- void initializeColorAnimators(AlignedTextView formula, CalculatorResult result);
-
- void initializeScales(AlignedTextView formula, CalculatorResult result);
-
- void initializeFormulaTranslationX(AlignedTextView formula);
-
- void initializeFormulaTranslationY(AlignedTextView formula, CalculatorResult result);
-
- void initializeResultTranslationX(CalculatorResult result);
-
- void initializeResultTranslationY(CalculatorResult result);
-
- float getResultTranslationX(float yFraction);
-
- float getResultTranslationY(float yFraction);
-
- float getResultScale(float yFraction);
-
- float getFormulaScale(float yFraction);
-
- float getFormulaTranslationX(float yFraction);
-
- float getFormulaTranslationY(float yFraction);
-
- float getDateTranslationY(float yFraction);
-
- float getHistoryElementTranslationY(float yFraction);
-
- // Return the lowest index of the first Viewholder to be translated upwards.
- // If there is no current expression, we translate all the viewholders; otherwise,
- // we start at index 1.
- int getFirstTranslatedViewHolderIndex();
- }
-
- // The default AnimationController when Display is in INPUT state and DisplayFormula is not
- // empty. There may or may not be a quick result.
- public class AnimationController implements DragController.AnimateTextInterface {
-
- public void initializeDisplayHeight() {
- // no-op
- }
-
- public void initializeColorAnimators(AlignedTextView formula, CalculatorResult result) {
- mFormulaStartColor = mDisplayFormula.getCurrentTextColor();
- mFormulaEndColor = formula.getCurrentTextColor();
-
- mResultStartColor = mDisplayResult.getCurrentTextColor();
- mResultEndColor = result.getCurrentTextColor();
- }
-
- public void initializeScales(AlignedTextView formula, CalculatorResult result) {
- // Calculate the scale for the text
- mFormulaScale = mDisplayFormula.getTextSize() / formula.getTextSize();
- }
-
- public void initializeFormulaTranslationY(AlignedTextView formula,
- CalculatorResult result) {
- if (mOneLine) {
- // Disregard result since we set it to GONE in the one-line case.
- mFormulaTranslationY =
- mDisplayFormula.getPaddingBottom() - formula.getPaddingBottom()
- - mBottomPaddingHeight;
- } else {
- // Baseline of formula moves by the difference in formula bottom padding and the
- // difference in result height.
- mFormulaTranslationY =
- mDisplayFormula.getPaddingBottom() - formula.getPaddingBottom()
- + mDisplayResult.getHeight() - result.getHeight()
- - mBottomPaddingHeight;
- }
- }
-
- public void initializeFormulaTranslationX(AlignedTextView formula) {
- // Right border of formula moves by the difference in formula end padding.
- mFormulaTranslationX = mDisplayFormula.getPaddingEnd() - formula.getPaddingEnd();
- }
-
- public void initializeResultTranslationY(CalculatorResult result) {
- // Baseline of result moves by the difference in result bottom padding.
- mResultTranslationY = mDisplayResult.getPaddingBottom() - result.getPaddingBottom()
- - mBottomPaddingHeight;
- }
-
- public void initializeResultTranslationX(CalculatorResult result) {
- mResultTranslationX = mDisplayResult.getPaddingEnd() - result.getPaddingEnd();
- }
-
- public float getResultTranslationX(float yFraction) {
- return mResultTranslationX * (yFraction - 1f);
- }
-
- public float getResultTranslationY(float yFraction) {
- return mResultTranslationY * (yFraction - 1f);
- }
-
- public float getResultScale(float yFraction) {
- return 1f;
- }
-
- public float getFormulaScale(float yFraction) {
- return mFormulaScale + (1f - mFormulaScale) * yFraction;
- }
-
- public float getFormulaTranslationX(float yFraction) {
- return mFormulaTranslationX * (yFraction - 1f);
- }
-
- public float getFormulaTranslationY(float yFraction) {
- // Scale linearly between -FormulaTranslationY and 0.
- return mFormulaTranslationY * (yFraction - 1f);
- }
-
- public float getDateTranslationY(float yFraction) {
- // We also want the date to start out above the visible screen with
- // this distance decreasing as it's pulled down.
- // Account for the scaled formula height.
- return -mToolbar.getHeight() * (1f - yFraction)
- + getFormulaTranslationY(yFraction)
- - mDisplayFormula.getHeight() /getFormulaScale(yFraction) * (1f - yFraction);
- }
-
- public float getHistoryElementTranslationY(float yFraction) {
- return getDateTranslationY(yFraction);
- }
-
- public int getFirstTranslatedViewHolderIndex() {
- return 1;
- }
- }
-
- // The default AnimationController when Display is in RESULT state.
- public class ResultAnimationController extends AnimationController
- implements DragController.AnimateTextInterface {
- @Override
- public void initializeScales(AlignedTextView formula, CalculatorResult result) {
- final float textSize = mDisplayResult.getTextSize() * mDisplayResult.getScaleX();
- mResultScale = textSize / result.getTextSize();
- mFormulaScale = 1f;
- }
-
- @Override
- public void initializeFormulaTranslationY(AlignedTextView formula,
- CalculatorResult result) {
- // Baseline of formula moves by the difference in formula bottom padding and the
- // difference in the result height.
- mFormulaTranslationY = mDisplayFormula.getPaddingBottom() - formula.getPaddingBottom()
- + mDisplayResult.getHeight() - result.getHeight()
- - mBottomPaddingHeight;
- }
-
- @Override
- public void initializeFormulaTranslationX(AlignedTextView formula) {
- // Right border of formula moves by the difference in formula end padding.
- mFormulaTranslationX = mDisplayFormula.getPaddingEnd() - formula.getPaddingEnd();
- }
-
- @Override
- public void initializeResultTranslationY(CalculatorResult result) {
- // Baseline of result moves by the difference in result bottom padding.
- mResultTranslationY = mDisplayResult.getPaddingBottom() - result.getPaddingBottom()
- - mDisplayResult.getTranslationY()
- - mBottomPaddingHeight;
- }
-
- @Override
- public void initializeResultTranslationX(CalculatorResult result) {
- mResultTranslationX = mDisplayResult.getPaddingEnd() - result.getPaddingEnd();
- }
-
- @Override
- public float getResultTranslationX(float yFraction) {
- return (mResultTranslationX * yFraction) - mResultTranslationX;
- }
-
- @Override
- public float getResultTranslationY(float yFraction) {
- return (mResultTranslationY * yFraction) - mResultTranslationY;
- }
-
- @Override
- public float getFormulaTranslationX(float yFraction) {
- return (mFormulaTranslationX * yFraction) -
- mFormulaTranslationX;
- }
-
- @Override
- public float getFormulaTranslationY(float yFraction) {
- return getDateTranslationY(yFraction);
- }
-
- @Override
- public float getResultScale(float yFraction) {
- return mResultScale - (mResultScale * yFraction) + yFraction;
- }
-
- @Override
- public float getFormulaScale(float yFraction) {
- return 1f;
- }
-
- @Override
- public float getDateTranslationY(float yFraction) {
- // We also want the date to start out above the visible screen with
- // this distance decreasing as it's pulled down.
- return -mToolbar.getHeight() * (1f - yFraction)
- + (mResultTranslationY * yFraction) - mResultTranslationY
- - mDisplayFormula.getPaddingTop() +
- (mDisplayFormula.getPaddingTop() * yFraction);
- }
-
- @Override
- public int getFirstTranslatedViewHolderIndex() {
- return 1;
- }
- }
-
- // The default AnimationController when Display is completely empty.
- public class EmptyAnimationController extends AnimationController
- implements DragController.AnimateTextInterface {
- @Override
- public void initializeDisplayHeight() {
- mDisplayHeight = mToolbar.getHeight() + mDisplayResult.getHeight()
- + mDisplayFormula.getHeight();
- }
-
- @Override
- public void initializeScales(AlignedTextView formula, CalculatorResult result) {
- // no-op
- }
-
- @Override
- public void initializeFormulaTranslationY(AlignedTextView formula,
- CalculatorResult result) {
- // no-op
- }
-
- @Override
- public void initializeFormulaTranslationX(AlignedTextView formula) {
- // no-op
- }
-
- @Override
- public void initializeResultTranslationY(CalculatorResult result) {
- // no-op
- }
-
- @Override
- public void initializeResultTranslationX(CalculatorResult result) {
- // no-op
- }
-
- @Override
- public float getResultTranslationX(float yFraction) {
- return 0f;
- }
-
- @Override
- public float getResultTranslationY(float yFraction) {
- return 0f;
- }
-
- @Override
- public float getFormulaScale(float yFraction) {
- return 1f;
- }
-
- @Override
- public float getDateTranslationY(float yFraction) {
- return 0f;
- }
-
- @Override
- public float getHistoryElementTranslationY(float yFraction) {
- return -mDisplayHeight * (1f - yFraction) - mBottomPaddingHeight;
- }
-
- @Override
- public int getFirstTranslatedViewHolderIndex() {
- return 0;
- }
- }
-}
diff --git a/src/com/android/calculator2/DragLayout.java b/src/com/android/calculator2/DragLayout.java
deleted file mode 100644
index 74b0a8e..0000000
--- a/src/com/android/calculator2/DragLayout.java
+++ /dev/null
@@ -1,357 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.calculator2;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.os.Bundle;
-import android.os.Parcelable;
-import androidx.core.view.ViewCompat;
-import androidx.customview.widget.ViewDragHelper;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.CopyOnWriteArrayList;
-
-public class DragLayout extends ViewGroup {
-
- private static final double AUTO_OPEN_SPEED_LIMIT = 600.0;
- private static final String KEY_IS_OPEN = "IS_OPEN";
- private static final String KEY_SUPER_STATE = "SUPER_STATE";
-
- private FrameLayout mHistoryFrame;
- private ViewDragHelper mDragHelper;
-
- // No concurrency; allow modifications while iterating.
- private final List<DragCallback> mDragCallbacks = new CopyOnWriteArrayList<>();
- private CloseCallback mCloseCallback;
-
- private final Map<Integer, PointF> mLastMotionPoints = new HashMap<>();
- private final Rect mHitRect = new Rect();
-
- private int mVerticalRange;
- private boolean mIsOpen;
-
- public DragLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- protected void onFinishInflate() {
- mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
- mHistoryFrame = (FrameLayout) findViewById(R.id.history_frame);
- super.onFinishInflate();
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- measureChildren(widthMeasureSpec, heightMeasureSpec);
- }
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- int displayHeight = 0;
- for (DragCallback c : mDragCallbacks) {
- displayHeight = Math.max(displayHeight, c.getDisplayHeight());
- }
- mVerticalRange = getHeight() - displayHeight;
-
- final int childCount = getChildCount();
- for (int i = 0; i < childCount; ++i) {
- final View child = getChildAt(i);
-
- int top = 0;
- if (child == mHistoryFrame) {
- if (mDragHelper.getCapturedView() == mHistoryFrame
- && mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE) {
- top = child.getTop();
- } else {
- top = mIsOpen ? 0 : -mVerticalRange;
- }
- }
- child.layout(0, top, child.getMeasuredWidth(), top + child.getMeasuredHeight());
- }
- }
-
- @Override
- protected Parcelable onSaveInstanceState() {
- final Bundle bundle = new Bundle();
- bundle.putParcelable(KEY_SUPER_STATE, super.onSaveInstanceState());
- bundle.putBoolean(KEY_IS_OPEN, mIsOpen);
- return bundle;
- }
-
- @Override
- protected void onRestoreInstanceState(Parcelable state) {
- if (state instanceof Bundle) {
- final Bundle bundle = (Bundle) state;
- mIsOpen = bundle.getBoolean(KEY_IS_OPEN);
- mHistoryFrame.setVisibility(mIsOpen ? View.VISIBLE : View.INVISIBLE);
- for (DragCallback c : mDragCallbacks) {
- c.onInstanceStateRestored(mIsOpen);
- }
-
- state = bundle.getParcelable(KEY_SUPER_STATE);
- }
- super.onRestoreInstanceState(state);
- }
-
- private void saveLastMotion(MotionEvent event) {
- final int action = event.getActionMasked();
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- case MotionEvent.ACTION_POINTER_DOWN: {
- final int actionIndex = event.getActionIndex();
- final int pointerId = event.getPointerId(actionIndex);
- final PointF point = new PointF(event.getX(actionIndex), event.getY(actionIndex));
- mLastMotionPoints.put(pointerId, point);
- break;
- }
- case MotionEvent.ACTION_MOVE: {
- for (int i = event.getPointerCount() - 1; i >= 0; --i) {
- final int pointerId = event.getPointerId(i);
- final PointF point = mLastMotionPoints.get(pointerId);
- if (point != null) {
- point.set(event.getX(i), event.getY(i));
- }
- }
- break;
- }
- case MotionEvent.ACTION_POINTER_UP: {
- final int actionIndex = event.getActionIndex();
- final int pointerId = event.getPointerId(actionIndex);
- mLastMotionPoints.remove(pointerId);
- break;
- }
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_CANCEL: {
- mLastMotionPoints.clear();
- break;
- }
- }
- }
-
- @Override
- public boolean onInterceptTouchEvent(MotionEvent event) {
- saveLastMotion(event);
- return mDragHelper.shouldInterceptTouchEvent(event);
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- // Workaround: do not process the error case where multi-touch would cause a crash.
- if (event.getActionMasked() == MotionEvent.ACTION_MOVE
- && mDragHelper.getViewDragState() == ViewDragHelper.STATE_DRAGGING
- && mDragHelper.getActivePointerId() != ViewDragHelper.INVALID_POINTER
- && event.findPointerIndex(mDragHelper.getActivePointerId()) == -1) {
- mDragHelper.cancel();
- return false;
- }
-
- saveLastMotion(event);
-
- mDragHelper.processTouchEvent(event);
- return true;
- }
-
- @Override
- public void computeScroll() {
- if (mDragHelper.continueSettling(true)) {
- ViewCompat.postInvalidateOnAnimation(this);
- }
- }
-
- private void onStartDragging() {
- for (DragCallback c : mDragCallbacks) {
- c.onStartDraggingOpen();
- }
- mHistoryFrame.setVisibility(VISIBLE);
- }
-
- public boolean isViewUnder(View view, int x, int y) {
- view.getHitRect(mHitRect);
- offsetDescendantRectToMyCoords((View) view.getParent(), mHitRect);
- return mHitRect.contains(x, y);
- }
-
- public boolean isMoving() {
- final int draggingState = mDragHelper.getViewDragState();
- return draggingState == ViewDragHelper.STATE_DRAGGING
- || draggingState == ViewDragHelper.STATE_SETTLING;
- }
-
- public boolean isOpen() {
- return mIsOpen;
- }
-
- public void setClosed() {
- mIsOpen = false;
- mHistoryFrame.setVisibility(View.INVISIBLE);
- if (mCloseCallback != null) {
- mCloseCallback.onClose();
- }
- }
-
- public Animator createAnimator(boolean toOpen) {
- if (mIsOpen == toOpen) {
- return ValueAnimator.ofFloat(0f, 1f).setDuration(0L);
- }
-
- mIsOpen = toOpen;
- mHistoryFrame.setVisibility(VISIBLE);
-
- final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animation) {
- mDragHelper.cancel();
- mDragHelper.smoothSlideViewTo(mHistoryFrame, 0, mIsOpen ? 0 : -mVerticalRange);
- }
- });
-
- return animator;
- }
-
- public void setCloseCallback(CloseCallback callback) {
- mCloseCallback = callback;
- }
-
- public void addDragCallback(DragCallback callback) {
- mDragCallbacks.add(callback);
- }
-
- public void removeDragCallback(DragCallback callback) {
- mDragCallbacks.remove(callback);
- }
-
- /**
- * Callback when the layout is closed.
- * We use this to pop the HistoryFragment off the backstack.
- * We can't use a method in DragCallback because we get ConcurrentModificationExceptions on
- * mDragCallbacks when executePendingTransactions() is called for popping the fragment off the
- * backstack.
- */
- public interface CloseCallback {
- void onClose();
- }
-
- /**
- * Callbacks for coordinating with the RecyclerView or HistoryFragment.
- */
- public interface DragCallback {
- // Callback when a drag to open begins.
- void onStartDraggingOpen();
-
- // Callback in onRestoreInstanceState.
- void onInstanceStateRestored(boolean isOpen);
-
- // Animate the RecyclerView text.
- void whileDragging(float yFraction);
-
- // Whether we should allow the view to be dragged.
- boolean shouldCaptureView(View view, int x, int y);
-
- int getDisplayHeight();
- }
-
- public class DragHelperCallback extends ViewDragHelper.Callback {
- @Override
- public void onViewDragStateChanged(int state) {
- // The view stopped moving.
- if (state == ViewDragHelper.STATE_IDLE
- && mDragHelper.getCapturedView().getTop() < -(mVerticalRange / 2)) {
- setClosed();
- }
- }
-
- @Override
- public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
- for (DragCallback c : mDragCallbacks) {
- // Top is between [-mVerticalRange, 0].
- c.whileDragging(1f + (float) top / mVerticalRange);
- }
- }
-
- @Override
- public int getViewVerticalDragRange(View child) {
- return mVerticalRange;
- }
-
- @Override
- public boolean tryCaptureView(View view, int pointerId) {
- final PointF point = mLastMotionPoints.get(pointerId);
- if (point == null) {
- return false;
- }
-
- final int x = (int) point.x;
- final int y = (int) point.y;
-
- for (DragCallback c : mDragCallbacks) {
- if (!c.shouldCaptureView(view, x, y)) {
- return false;
- }
- }
- return true;
- }
-
- @Override
- public int clampViewPositionVertical(View child, int top, int dy) {
- return Math.max(Math.min(top, 0), -mVerticalRange);
- }
-
- @Override
- public void onViewCaptured(View capturedChild, int activePointerId) {
- super.onViewCaptured(capturedChild, activePointerId);
-
- if (!mIsOpen) {
- mIsOpen = true;
- onStartDragging();
- }
- }
-
- @Override
- public void onViewReleased(View releasedChild, float xvel, float yvel) {
- final boolean settleToOpen;
- if (yvel > AUTO_OPEN_SPEED_LIMIT) {
- // Speed has priority over position.
- settleToOpen = true;
- } else if (yvel < -AUTO_OPEN_SPEED_LIMIT) {
- settleToOpen = false;
- } else {
- settleToOpen = releasedChild.getTop() > -(mVerticalRange / 2);
- }
-
- // If the view is not visible, then settle it closed, not open.
- if (mDragHelper.settleCapturedViewAt(0, settleToOpen && mIsOpen ? 0
- : -mVerticalRange)) {
- ViewCompat.postInvalidateOnAnimation(DragLayout.this);
- }
- }
- }
-}
diff --git a/src/com/android/calculator2/Evaluator.java b/src/com/android/calculator2/Evaluator.java
deleted file mode 100644
index eee9fa8..0000000
--- a/src/com/android/calculator2/Evaluator.java
+++ /dev/null
@@ -1,1963 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.calculator2;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Handler;
-import android.preference.PreferenceManager;
-import androidx.annotation.NonNull;
-import androidx.annotation.StringRes;
-import androidx.annotation.VisibleForTesting;
-import android.text.Spannable;
-import android.util.Log;
-
-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.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.
- * 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 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() 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
- * allow for some scrolling without recomputation and to minimize the chance of digits flipping
- * from "0000" to "9999". The best known result approximation is maintained as a string by
- * mResultString (and often in a different format by the CR representation of the result). When
- * we are in danger of not having digits to display in response to further scrolling, we also
- * initiate a background computation to higher precision, as if we had generated placeholder
- * characters.
- *
- * The code is designed to ensure that the error in the displayed result (excluding any
- * placeholder characters) is always strictly less than 1 in the last displayed digit. Typically
- * we actually display a prefix of a result that has this property and additionally is computed to
- * a significantly higher precision. Thus we almost always round correctly towards zero. (Fully
- * correct rounding towards zero is not computable, at least given our representation.)
- *
- * Initial expression evaluation may time out. This may happen in the case of domain errors such
- * as division by zero, or for large computations. We do not currently time out reevaluations to
- * higher precision, since the original evaluation precluded a domain error that could result in
- * non-termination. (We may discover that a presumed zero result is actually slightly negative
- * when re-evaluated; but that results in an exception, which we can handle.) The user can abort
- * either kind of computation.
- *
- * We ensure that only one evaluation of either kind (AsyncEvaluator or AsyncReevaluator) is
- * running at a time.
- */
-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. (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
- // avoid generating placeholder digits, but should only be displayed briefly while computing.
- private static final int EXTRA_DIGITS = 20;
-
- // We adjust EXTRA_DIGITS by adding the length of the previous result divided by
- // EXTRA_DIVISOR. This helps hide recompute latency when long results are requested;
- // We start the recomputation substantially before the need is likely to be visible.
- private static final int EXTRA_DIVISOR = 5;
-
- // In addition to insisting on extra digits (see above), we minimize reevaluation
- // frequency by precomputing an extra PRECOMPUTE_DIGITS
- // + <current_precision_offset>/PRECOMPUTE_DIVISOR digits, whenever we are forced to
- // reevaluate. The last term is dropped if prec < 0.
- private static final int PRECOMPUTE_DIGITS = 30;
- private static final int PRECOMPUTE_DIVISOR = 5;
-
- // Initial evaluation precision. Enough to guarantee that we can compute the short
- // representation, and that we rarely have to evaluate nonzero results to MAX_MSD_PREC_OFFSET.
- // It also helps if this is at least EXTRA_DIGITS + display width, so that we don't
- // immediately need a second evaluation.
- private static final int INIT_PREC = 50;
-
- // 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. 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 = 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;
-
- // Listener that reports changes to the state (empty/filled) of memory. Protected for testing.
- private Callback mCallback;
-
- // Context for database helper.
- private Context mContext;
-
- // A hopefully unique name associated with mSaved.
- private String mSavedName;
-
- // 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;
-
- public static final int INVALID_MSD = Integer.MAX_VALUE;
-
- // Used to represent an erroneous result or a required evaluation. Not displayed.
- private static final String ERRONEOUS_RESULT = "ERR";
-
- /**
- * 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>();
- }
-
- // 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;
- }
-
- private ConcurrentHashMap<Long, ExprInfo> mExprs = new ConcurrentHashMap<Long, ExprInfo>();
-
- // The database holding persistent expressions.
- private ExpressionDB mExprDB;
-
- private ExprInfo mMainExpr; // == mExprs.get(MAIN_INDEX)
-
- private SharedPreferences mSharedPrefs;
-
- private final Handler mTimeoutHandler; // Used to schedule evaluation timeouts.
-
- private void setMainExpr(ExprInfo expr) {
- mMainExpr = expr;
- mExprs.put(MAIN_INDEX, expr);
- }
-
- Evaluator(Context context) {
- mContext = context;
- setMainExpr(new ExprInfo(new CalculatorExpr(), false));
- mSavedName = "none";
- mTimeoutHandler = new Handler();
-
- 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;
- }
-
- /**
- * Result of initial asynchronous result computation.
- * Represents either an error or a result computed to an initial evaluation precision.
- */
- private static class InitialResult {
- public final int errorResourceId; // Error string or INVALID_RES_ID.
- public final UnifiedReal val; // Constructive real value.
- public final String newResultString; // Null iff it can't be computed.
- public final int newResultStringOffset;
- public final int initDisplayOffset;
- InitialResult(UnifiedReal v, String s, int p, int idp) {
- errorResourceId = Calculator.INVALID_RES_ID;
- val = v;
- newResultString = s;
- newResultStringOffset = p;
- initDisplayOffset = idp;
- }
- InitialResult(int errorId) {
- errorResourceId = errorId;
- val = UnifiedReal.ZERO;
- newResultString = "BAD";
- newResultStringOffset = 0;
- initDisplayOffset = 0;
- }
- boolean isError() {
- return errorResourceId != Calculator.INVALID_RES_ID;
- }
- }
-
- private void displayCancelledMessage() {
- if (mCallback != null) {
- mCallback.showMessageDialog(0, R.string.cancelled, 0, null);
- }
- }
-
- // Timeout handling.
- // Expressions are evaluated with a sort timeout or a long timeout.
- // Each implies different maxima on both computation time and bit length.
- // We recheck bit length separetly to avoid wasting time on decimal conversions that are
- // destined to fail.
-
- /**
- * Return the timeout in milliseconds.
- * @param longTimeout a long timeout is in effect
- */
- private long getTimeout(boolean longTimeout) {
- return longTimeout ? 15000 : 2000;
- // Exceeding a few tens of seconds increases the risk of running out of memory
- // and impacting the rest of the system.
- }
-
- /**
- * Return the maximum number of bits in the result. Longer results are assumed to time out.
- * @param longTimeout a long timeout is in effect
- */
- private int getMaxResultBits(boolean longTimeout) {
- return longTimeout ? 700000 : 240000;
- }
-
- /**
- * Timeout for unrequested, speculative evaluations, in milliseconds.
- */
- 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 static final int QUICK_MAX_RESULT_BITS = 150000;
-
- 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() {
- mMainExpr.mLongTimeout = true;
- }
-
- /**
- * Compute initial cache contents and result when we're good and ready.
- * We leave the expression display up, with scrolling disabled, until this computation
- * completes. Can result in an error display if something goes wrong. By default we set a
- * timeout to catch runaway computations.
- */
- class AsyncEvaluator extends AsyncTask<Void, Void, InitialResult> {
- private boolean mDm; // degrees
- public boolean mRequired; // Result was requested by user.
- private boolean mQuiet; // Suppress cancellation message.
- private Runnable mTimeoutRunnable = null;
- 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 || mIndex != MAIN_INDEX;
- mExprInfo = mExprs.get(mIndex);
- if (mExprInfo.mEvaluator != null) {
- throw new AssertionError("Evaluation already in progress!");
- }
- }
-
- private void handleTimeout() {
- // Runs in UI thread.
- boolean running = (getStatus() != AsyncTask.Status.FINISHED);
- if (running && cancel(true)) {
- 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(mExprInfo.mLongTimeout);
- }
- }
- }
-
- private void suppressCancelMessage() {
- mQuiet = true;
- }
-
- @Override
- protected void onPreExecute() {
- 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();
- }
- };
- mTimeoutHandler.removeCallbacks(mTimeoutRunnable);
- mTimeoutHandler.postDelayed(mTimeoutRunnable, timeout);
- }
-
- /**
- * Is a computed result too big for decimal conversion?
- */
- private boolean isTooBig(UnifiedReal res) {
- final int maxBits = mRequired ? getMaxResultBits(mExprInfo.mLongTimeout)
- : QUICK_MAX_RESULT_BITS;
- return res.approxWholeNumberBitsGreaterThan(maxBits);
- }
-
- @Override
- protected InitialResult doInBackground(Void... nothing) {
- try {
- // 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);
- }
- int precOffset = INIT_PREC;
- String initResult = res.toStringTruncated(precOffset);
- int msd = getMsdIndexOf(initResult);
- if (msd == INVALID_MSD) {
- int leadingZeroBits = res.leadingBinaryZeroes();
- if (leadingZeroBits < QUICK_MAX_RESULT_BITS) {
- // Enough initial nonzero digits for most displays.
- precOffset = 30 +
- (int)Math.ceil(Math.log(2.0d) / Math.log(10.0d) * leadingZeroBits);
- initResult = res.toStringTruncated(precOffset);
- msd = getMsdIndexOf(initResult);
- if (msd == INVALID_MSD) {
- throw new AssertionError("Impossible zero result");
- }
- } else {
- // Just try once more at higher fixed precision.
- precOffset = MAX_MSD_PREC_OFFSET;
- initResult = res.toStringTruncated(precOffset);
- msd = getMsdIndexOf(initResult);
- }
- }
- final int lsdOffset = getLsdOffset(res, initResult, initResult.indexOf('.'));
- final int initDisplayOffset = getPreferredPrec(initResult, msd, lsdOffset,
- mCharMetricsInfo);
- final int newPrecOffset = initDisplayOffset + EXTRA_DIGITS;
- if (newPrecOffset > precOffset) {
- precOffset = newPrecOffset;
- initResult = res.toStringTruncated(precOffset);
- }
- return new InitialResult(res, initResult, precOffset, initDisplayOffset);
- } catch (CalculatorExpr.SyntaxException e) {
- return new InitialResult(R.string.error_syntax);
- } catch (UnifiedReal.ZeroDivisionException e) {
- return new InitialResult(R.string.error_zero_divide);
- } catch(ArithmeticException e) {
- return new InitialResult(R.string.error_nan);
- } catch(CR.PrecisionOverflowException e) {
- // Extremely unlikely unless we're actually dividing by zero or the like.
- return new InitialResult(R.string.error_overflow);
- } catch(CR.AbortedException e) {
- return new InitialResult(R.string.error_aborted);
- }
- }
-
- @Override
- protected void onPostExecute(InitialResult result) {
- mExprInfo.mEvaluator = null;
- mTimeoutHandler.removeCallbacks(mTimeoutRunnable);
- if (result.isError()) {
- if (result.errorResourceId == R.string.timeout) {
- // Emulating timeout due to large result.
- if (mRequired && mIndex == MAIN_INDEX) {
- displayTimeoutMessage(mExprs.get(mIndex).mLongTimeout);
- }
- mListener.onCancelled(mIndex);
- } else {
- if (mRequired) {
- mExprInfo.mResultString = ERRONEOUS_RESULT;
- }
- mListener.onError(mIndex, result.errorResourceId);
- }
- return;
- }
- // 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;
- 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.
- }
- mListener.onEvaluate(mIndex, initPrecOffset, mExprInfo.mMsdIndex, leastDigOffset,
- truncatedWholePart);
- }
-
- @Override
- protected void onCancelled(InitialResult result) {
- // Invoker resets mEvaluator.
- mTimeoutHandler.removeCallbacks(mTimeoutRunnable);
- if (!mQuiet) {
- displayCancelledMessage();
- } // Otherwise, if mRequired, timeout processing displayed message.
- mListener.onCancelled(mIndex);
- // Just drop the evaluation; Leave expression displayed.
- return;
- }
- }
-
- /**
- * Check whether a new higher precision result flips previously computed trailing 9s
- * to zeroes. If so, flip them back. Return the adjusted result.
- * Assumes newPrecOffset >= oldPrecOffset > 0.
- * Since our results are accurate to < 1 ulp, this can only happen if the true result
- * is less than the new result with trailing zeroes, and thus appending 9s to the
- * old result must also be correct. Such flips are impossible if the newly computed
- * digits consist of anything other than zeroes.
- * It is unclear that there are real cases in which this is necessary,
- * but we have failed to prove there aren't such cases.
- */
- @VisibleForTesting
- public static String unflipZeroes(String oldDigs, int oldPrecOffset, String newDigs,
- int newPrecOffset) {
- final int oldLen = oldDigs.length();
- if (oldDigs.charAt(oldLen - 1) != '9') {
- return newDigs;
- }
- final int newLen = newDigs.length();
- final int precDiff = newPrecOffset - oldPrecOffset;
- final int oldLastInNew = newLen - 1 - precDiff;
- if (newDigs.charAt(oldLastInNew) != '0') {
- return newDigs;
- }
- // Earlier digits could not have changed without a 0 to 9 or 9 to 0 flip at end.
- // The former is OK.
- if (!newDigs.substring(newLen - precDiff).equals(StringUtils.repeat('0', precDiff))) {
- throw new AssertionError("New approximation invalidates old one!");
- }
- return oldDigs + StringUtils.repeat('9', precDiff);
- }
-
- /**
- * Result of asynchronous reevaluation.
- */
- private static class ReevalResult {
- public final String newResultString;
- public final int newResultStringOffset;
- ReevalResult(String s, int p) {
- newResultString = s;
- newResultStringOffset = p;
- }
- }
-
- /**
- * 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(mExprInfo.mVal.get().toStringTruncated(precOffset),
- precOffset);
- } catch(ArithmeticException e) {
- return null;
- } catch(CR.PrecisionOverflowException e) {
- return null;
- } catch(CR.AbortedException e) {
- // Should only happen if the task was cancelled, in which case we don't look at
- // the result.
- return null;
- }
- }
-
- @Override
- protected void onPostExecute(ReevalResult result) {
- if (result == null) {
- // 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.
- mExprInfo.mResultString = ERRONEOUS_RESULT;
- mListener.onError(mIndex, R.string.error_nan);
- } else {
- if (result.newResultStringOffset < mExprInfo.mResultStringOffset) {
- throw new AssertionError("Unexpected onPostExecute timing");
- }
- mExprInfo.mResultString = unflipZeroes(mExprInfo.mResultString,
- mExprInfo.mResultStringOffset, result.newResultString,
- result.newResultStringOffset);
- mExprInfo.mResultStringOffset = result.newResultStringOffset;
- mListener.onReevaluate(mIndex);
- }
- mExprInfo.mEvaluator = null;
- }
- // On cancellation we do nothing; invoker should have left no trace of us.
- }
-
- /**
- * 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(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.
- ei.mEvaluator.cancel(true);
- ei.mEvaluator = null;
- }
- AsyncReevaluator reEval = new AsyncReevaluator(index, listener);
- ei.mEvaluator = reEval;
- ei.mResultStringOffsetReq = precOffset + PRECOMPUTE_DIGITS;
- if (ei.mResultString != null) {
- ei.mResultStringOffsetReq += ei.mResultStringOffsetReq / PRECOMPUTE_DIVISOR;
- }
- reEval.execute(ei.mResultStringOffsetReq);
- }
-
- /**
- * Return the rightmost nonzero digit position, if any.
- * @param val UnifiedReal value of result.
- * @param cache Current cached decimal string representation of result.
- * @param decIndex Index of decimal point in cache.
- * @result Position of rightmost nonzero digit relative to decimal point.
- * Integer.MIN_VALUE if we cannot determine. Integer.MAX_VALUE if there is no lsd,
- * or we cannot determine it.
- */
- static int getLsdOffset(UnifiedReal val, String cache, int decIndex) {
- if (val.definitelyZero()) return Integer.MIN_VALUE;
- int result = val.digitsRequired();
- if (result == 0) {
- int i;
- for (i = -1; decIndex + i > 0 && cache.charAt(decIndex + i) == '0'; --i) { }
- result = i;
- }
- return result;
- }
-
- // TODO: We may want to consistently specify the position of the current result
- // window using the left-most visible digit index instead of the offset for the rightmost one.
- // It seems likely that would simplify the logic.
-
- /**
- * Retrieve the preferred precision "offset" for the currently displayed result.
- * May be called from non-UI thread.
- * @param cache Current approximation as string.
- * @param msd Position of most significant digit in result. Index in cache.
- * Can be INVALID_MSD if we haven't found it yet.
- * @param lastDigitOffset Position of least significant digit (1 = tenths digit)
- * or Integer.MAX_VALUE.
- */
- 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 = 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;
- // Don't display decimal point if result is an integer.
- if (lastDigitOffset == 0) {
- lastDigitOffset = -1;
- }
- if (lastDigitOffset != Integer.MAX_VALUE) {
- if (wholeSize <= lineLength - sepCharsNoDecimal && lastDigitOffset <= 0) {
- // Exact integer. Prefer to display as integer, without decimal point.
- return -1;
- }
- if (lastDigitOffset >= 0
- && wholeSize + lastDigitOffset + 1 /* decimal pt. */
- <= lineLength - sepCharsWithDecimal) {
- // Display full exact number without scientific notation.
- return lastDigitOffset;
- }
- }
- if (msd > wholeSize && msd <= wholeSize + EXP_COST + 1) {
- // Display number without scientific notation. Treat leading zero as msd.
- msd = wholeSize - 1;
- }
- if (msd > QUICK_MAX_RESULT_BITS) {
- // Display a probable but uncertain 0 as "0.000000000", without exponent. That's a
- // judgment call, but less likely to confuse naive users. A more informative and
- // confusing option would be to use a large negative exponent.
- // Treat extremely large msd values as unknown to avoid slow computations.
- return lineLength - 2;
- }
- // Return position corresponding to having msd at left, effectively presuming scientific
- // notation that preserves the left part of the result.
- // After adjustment for the space required by an exponent, evaluating to the resulting
- // precision should not overflow the display.
- int result = msd - wholeSize + lineLength - negative - 1;
- if (wholeSize <= lineLength - sepCharsNoDecimal) {
- // Fits without scientific notation; will need space for separators.
- if (wholeSize < lineLength - sepCharsWithDecimal) {
- result -= sepCharsWithDecimal;
- } else {
- result -= sepCharsNoDecimal;
- }
- }
- return result;
- }
-
- private static final int SHORT_TARGET_LENGTH = 8;
- private static final String SHORT_UNCERTAIN_ZERO = "0.00000" + KeyMaps.ELLIPSIS;
-
- /**
- * Get a short representation of the value represented by the string cache.
- * We try to match the CalculatorResult code when the result is finite
- * and small enough to suit our needs.
- * The result is not internationalized.
- * @param cache String approximation of value. Assumed to be long enough
- * that if it doesn't contain enough significant digits, we can
- * reasonably abbreviate as SHORT_UNCERTAIN_ZERO.
- * @param msdIndex Index of most significant digit in cache, or INVALID_MSD.
- * @param lsdOffset Position of least significant digit in finite representation,
- * relative to decimal point, or MAX_VALUE.
- */
- 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
- // is a bit flexible.
- // TODO: Think about refactoring this to remove partial redundancy with CalculatorResult.
- final int dotIndex = cache.indexOf('.');
- final int negative = cache.charAt(0) == '-' ? 1 : 0;
- final String negativeSign = negative == 1 ? "-" : "";
-
- // Ensure we don't have to worry about running off the end of cache.
- if (msdIndex >= cache.length() - SHORT_TARGET_LENGTH) {
- msdIndex = INVALID_MSD;
- }
- if (msdIndex == INVALID_MSD) {
- if (lsdOffset < INIT_PREC) {
- return "0";
- } else {
- return SHORT_UNCERTAIN_ZERO;
- }
- }
- // Avoid scientific notation for small numbers of zeros.
- // Instead stretch significant digits to include decimal point.
- if (lsdOffset < -1 && dotIndex - msdIndex + negative <= SHORT_TARGET_LENGTH
- && lsdOffset >= -CalculatorResult.MAX_TRAILING_ZEROES - 1) {
- // Whole number that fits in allotted space.
- // CalculatorResult would not use scientific notation either.
- lsdOffset = -1;
- }
- if (msdIndex > dotIndex) {
- if (msdIndex <= dotIndex + EXP_COST + 1) {
- // Preferred display format in this case is with leading zeroes, even if
- // it doesn't fit entirely. Replicate that here.
- msdIndex = dotIndex - 1;
- } else if (lsdOffset <= SHORT_TARGET_LENGTH - negative - 2
- && lsdOffset <= CalculatorResult.MAX_LEADING_ZEROES + 1) {
- // Fraction that fits entirely in allotted space.
- // CalculatorResult would not use scientific notation either.
- msdIndex = dotIndex -1;
- }
- }
- int exponent = dotIndex - msdIndex;
- if (exponent > 0) {
- // Adjust for the fact that the decimal point itself takes space.
- exponent--;
- }
- if (lsdOffset != Integer.MAX_VALUE) {
- final int lsdIndex = dotIndex + lsdOffset;
- final int totalDigits = lsdIndex - msdIndex + negative + 1;
- if (totalDigits <= SHORT_TARGET_LENGTH && dotIndex > msdIndex && lsdOffset >= -1) {
- // Fits, no exponent needed.
- final String wholeWithCommas = StringUtils.addCommas(cache, msdIndex, dotIndex);
- return negativeSign + wholeWithCommas + cache.substring(dotIndex, lsdIndex + 1);
- }
- if (totalDigits <= SHORT_TARGET_LENGTH - 3) {
- return negativeSign + cache.charAt(msdIndex) + "."
- + cache.substring(msdIndex + 1, lsdIndex + 1) + "E" + exponent;
- }
- }
- // We need to abbreviate.
- if (dotIndex > msdIndex && dotIndex < msdIndex + SHORT_TARGET_LENGTH - negative - 1) {
- final String wholeWithCommas = StringUtils.addCommas(cache, msdIndex, dotIndex);
- return negativeSign + wholeWithCommas
- + cache.substring(dotIndex, msdIndex + SHORT_TARGET_LENGTH - negative - 1)
- + KeyMaps.ELLIPSIS;
- }
- // Need abbreviation + exponent
- return negativeSign + cache.charAt(msdIndex) + "."
- + cache.substring(msdIndex + 1, msdIndex + SHORT_TARGET_LENGTH - negative - 4)
- + KeyMaps.ELLIPSIS + "E" + exponent;
- }
-
- /**
- * Return the most significant digit index in the given numeric string.
- * Return INVALID_MSD if there are not enough digits to prove the numeric value is
- * different from zero. As usual, we assume an error of strictly less than 1 ulp.
- */
- public static int getMsdIndexOf(String s) {
- final int len = s.length();
- int nonzeroIndex = -1;
- for (int i = 0; i < len; ++i) {
- char c = s.charAt(i);
- if (c != '-' && c != '.' && c != '0') {
- nonzeroIndex = i;
- break;
- }
- }
- if (nonzeroIndex >= 0 && (nonzeroIndex < len - 1 || s.charAt(nonzeroIndex) != '1')) {
- return nonzeroIndex;
- } else {
- return INVALID_MSD;
- }
- }
-
- /**
- * 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(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 (ei.mResultString.charAt(ei.mMsdIndex) == '0') {
- ei.mMsdIndex++;
- }
- return ei.mMsdIndex;
- }
- if (ei.mVal.get().definitelyZero()) {
- return INVALID_MSD; // None exists
- }
- int result = INVALID_MSD;
- if (ei.mResultString != null) {
- result = ei.mMsdIndex = getMsdIndexOf(ei.mResultString);
- }
- return result;
- }
-
- // Refuse to scroll past the point at which this many digits from the whole number
- // part of the result are still displayed. Avoids sily displays like 1E1.
- private static final int MIN_DISPLAYED_DIGS = 5;
-
- /**
- * 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 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.
- * (precOffset[0] = 0 means we include the decimal point, but nothing to the right.
- * precOffset[0] = -1 means we drop the decimal point and start at the ones position. Should
- * not be invoked before the onEvaluate() callback is received. This essentially just returns
- * a substring of the full result; a leading minus sign or leading digits can be dropped.
- * 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(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 (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(index, currentPrecOffset + EXTRA_DIGITS + ei.mResultString.length()
- / EXTRA_DIVISOR, listener);
- }
- // Compute an appropriate substring of mResultString. Pad if necessary.
- 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 - ei.mResultStringOffset;
- // includes 1 for dec. pt
- if (myNegative) {
- --integralDigits;
- }
- int minPrecOffset = Math.min(MIN_DISPLAYED_DIGS - integralDigits, -1);
- currentPrecOffset = Math.min(Math.max(currentPrecOffset, minPrecOffset),
- maxPrecOffset);
- precOffset[0] = currentPrecOffset;
- 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 - ei.mResultStringOffset, maxDigs);
- }
- int endIndex = len - extraDigs;
- if (endIndex < 1) {
- return " ";
- }
- int startIndex = Math.max(endIndex + deficit - maxDigs, 0);
- 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.
- // Since we always compute past the decimal point, this never fills in the spot
- // where the decimal point should go, and we can otherwise treat placeholders
- // as though they were digits.
- }
- return result;
- }
-
- /**
- * Clear the cache for the main expression.
- */
- private void clearMainCache() {
- mMainExpr.mVal.set(null);
- mMainExpr.mResultString = null;
- mMainExpr.mResultStringOffset = mMainExpr.mResultStringOffsetReq = 0;
- mMainExpr.mMsdIndex = INVALID_MSD;
- }
-
-
- public void clearMain() {
- mMainExpr.mExpr.clear();
- mHasTrigFuncs = false;
- clearMainCache();
- mMainExpr.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 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(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;
- }
- }
-
- /**
- * Notify listener of a previously completed evaluation.
- */
- 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;
- }
- evaluateResult(index, listener, cmi, false);
- }
-
- /**
- * 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(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 {
- 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 evaluationInProgress(long index) {
- ExprInfo ei = mExprs.get(index);
- return ei != null && ei.mEvaluator != null;
- }
-
- /**
- * Cancel any current background task associated with the given ExprInfo.
- * @param quiet suppress cancellation message
- * @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;
- }
- }
- return false;
- }
-
- /**
- * 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 {
- 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);
- }
- }
-
- /**
- * Save the evaluator state, including the expression and any saved value.
- */
- public void saveInstanceState(DataOutput out) {
- try {
- out.writeBoolean(mMainExpr.mDegreeMode);
- out.writeBoolean(mMainExpr.mLongTimeout);
- mMainExpr.mExpr.write(out);
- } catch (IOException e) {
- Log.v("Calculator", "Exception while saving state:\n" + e);
- }
- }
-
-
- /**
- * 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
- */
- public boolean append(int id) {
- if (id == R.id.fun_10pow) {
- add10pow(); // Handled as macro expansion.
- return true;
- } else {
- mChangedValue = mChangedValue || !KeyMaps.isBinary(id);
- if (mMainExpr.mExpr.add(id)) {
- if (!mHasTrigFuncs) {
- mHasTrigFuncs = KeyMaps.isTrigFunc(id);
- }
- return true;
- } else {
- return false;
- }
- }
- }
-
- /**
- * Delete last taken from main expression.
- */
- public void delete() {
- mChangedValue = true;
- mMainExpr.mExpr.delete();
- if (mMainExpr.mExpr.isEmpty()) {
- mMainExpr.mLongTimeout = false;
- }
- mHasTrigFuncs = hasTrigFuncs();
- }
-
- /**
- * Set degree mode for main expression.
- */
- public void setDegreeMode(boolean degreeMode) {
- mChangedValue = true;
- mMainExpr.mDegreeMode = degreeMode;
-
- mSharedPrefs.edit()
- .putBoolean(KEY_PREF_DEGREE_MODE, degreeMode)
- .apply();
- }
-
- /**
- * 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 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 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;
- }
-
- /**
- * 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(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.
- }
-
- /**
- * Mark the expression as changed, preventing next evaluation request from being ignored.
- */
- public void touch() {
- 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();
- }
- }
-
- /**
- * 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.
- */
- private boolean copyToSaved(long index) {
- if (mExprs.get(index).mResultString == null
- || mExprs.get(index).mResultString == ERRONEOUS_RESULT) {
- return false;
- }
- 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)
- .build();
- }
-
- /**
- * 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(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();
- DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
- df.setTimeZone(tz);
- 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 mSavedIndex != 0 && uri.equals(uriForSaved());
- }
-
- /**
- * 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;
- mMainExpr.mLongTimeout |= ei.mLongTimeout;
- CalculatorExpr collapsed = getCollapsedExpr(index);
- if (collapsed != null) {
- mMainExpr.mExpr.append(getCollapsedExpr(index));
- }
- }
-
- /**
- * Add the power of 10 operator to the main expression.
- * This is treated essentially as a macro expansion.
- */
- private void add10pow() {
- CalculatorExpr ten = new CalculatorExpr();
- ten.add(R.id.digit_1);
- ten.add(R.id.digit_0);
- mChangedValue = true; // For consistency. Reevaluation is probably not useful.
- mMainExpr.mExpr.append(ten);
- mMainExpr.mExpr.add(R.id.op_pow);
- }
-
- /**
- * 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 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();
- }
- }
-
- /**
- * Does the current main expression contain trig functions?
- * Might its value depend on DEG/RAD mode?
- */
- public boolean hasTrigFuncs() {
- return mHasTrigFuncs;
- }
-
- /**
- * Maximum number of characters in a scientific notation exponent.
- */
- private static final int MAX_EXP_CHARS = 8;
-
- /**
- * Return the index of the character after the exponent starting at s[offset].
- * Return offset if there is no exponent at that position.
- * Exponents have syntax E[-]digit* . "E2" and "E-2" are valid. "E+2" and "e2" are not.
- * We allow any Unicode digits, and either of the commonly used minus characters.
- */
- public static int exponentEnd(String s, int offset) {
- int i = offset;
- int len = s.length();
- if (i >= len - 1 || s.charAt(i) != 'E') {
- return offset;
- }
- ++i;
- if (KeyMaps.keyForChar(s.charAt(i)) == R.id.op_sub) {
- ++i;
- }
- if (i == len || !Character.isDigit(s.charAt(i))) {
- return offset;
- }
- ++i;
- while (i < len && Character.isDigit(s.charAt(i))) {
- ++i;
- if (i > offset + MAX_EXP_CHARS) {
- return offset;
- }
- }
- return i;
- }
-
- /**
- * Add the exponent represented by s[begin..end) to the constant at the end of current
- * expression.
- * The end of the current expression must be a constant. Exponents have the same syntax as
- * for exponentEnd().
- */
- public void addExponent(String s, int begin, int end) {
- int sign = 1;
- int exp = 0;
- int i = begin + 1;
- // We do the decimal conversion ourselves to exactly match exponentEnd() conventions
- // and handle various kinds of digits on input. Also avoids allocation.
- if (KeyMaps.keyForChar(s.charAt(i)) == R.id.op_sub) {
- sign = -1;
- ++i;
- }
- for (; i < end; ++i) {
- exp = 10 * exp + Character.digit(s.charAt(i), 10);
- }
- 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);
- }
-}
diff --git a/src/com/android/calculator2/ExpressionDB.java b/src/com/android/calculator2/ExpressionDB.java
deleted file mode 100644
index 9a0f8ec..0000000
--- a/src/com/android/calculator2/ExpressionDB.java
+++ /dev/null
@@ -1,619 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// We make some strong assumptions about the databases we manipulate.
-// We maintain a single table containg expressions, their indices in the sequence of
-// expressions, and some data associated with each expression.
-// All indices are used, except for a small gap around zero. New rows are added
-// either just below the current minimum (negative) index, or just above the current
-// maximum index. Currently no rows are deleted unless we clear the whole table.
-
-// TODO: Especially if we notice serious performance issues on rotation in the history
-// view, we may need to use a CursorLoader or some other scheme to preserve the database
-// across rotations.
-// TODO: We may want to switch to a scheme in which all expressions saved in the database have
-// a positive index, and a flag indicates whether the expression is displayed as part of
-// the history or not. That would avoid potential thrashing between CursorWindows when accessing
-// with a negative index. It would also make it easy to sort expressions in dependency order,
-// which helps with avoiding deep recursion during evaluation. But it makes the history UI
-// implementation more complicated. It should be possible to make this change without a
-// database version bump.
-
-// This ensures strong thread-safety, i.e. each call looks atomic to other threads. We need some
-// such property, since expressions may be read by one thread while the main thread is updating
-// another expression.
-
-package com.android.calculator2;
-
-import android.app.Activity;
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.AbstractWindowedCursor;
-import android.database.Cursor;
-import android.database.CursorWindow;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteException;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.os.AsyncTask;
-import android.provider.BaseColumns;
-import android.util.Log;
-import android.view.View;
-
-public class ExpressionDB {
- private final boolean CONTINUE_WITH_BAD_DB = false;
-
- /* Table contents */
- public static class ExpressionEntry implements BaseColumns {
- public static final String TABLE_NAME = "expressions";
- public static final String COLUMN_NAME_EXPRESSION = "expression";
- public static final String COLUMN_NAME_FLAGS = "flags";
- // Time stamp as returned by currentTimeMillis().
- public static final String COLUMN_NAME_TIMESTAMP = "timeStamp";
- }
-
- /* Data to be written to or read from a row in the table */
- public static class RowData {
- private static final int DEGREE_MODE = 2;
- private static final int LONG_TIMEOUT = 1;
- public final byte[] mExpression;
- public final int mFlags;
- public long mTimeStamp; // 0 ==> this and next field to be filled in when written.
- private static int flagsFromDegreeAndTimeout(Boolean DegreeMode, Boolean LongTimeout) {
- return (DegreeMode ? DEGREE_MODE : 0) | (LongTimeout ? LONG_TIMEOUT : 0);
- }
- private boolean degreeModeFromFlags(int flags) {
- return (flags & DEGREE_MODE) != 0;
- }
- private boolean longTimeoutFromFlags(int flags) {
- return (flags & LONG_TIMEOUT) != 0;
- }
- private static final int MILLIS_IN_15_MINS = 15 * 60 * 1000;
- private RowData(byte[] expr, int flags, long timeStamp) {
- mExpression = expr;
- mFlags = flags;
- mTimeStamp = timeStamp;
- }
- /**
- * More client-friendly constructor that hides implementation ugliness.
- * utcOffset here is uncompressed, in milliseconds.
- * A zero timestamp will cause it to be automatically filled in.
- */
- public RowData(byte[] expr, boolean degreeMode, boolean longTimeout, long timeStamp) {
- this(expr, flagsFromDegreeAndTimeout(degreeMode, longTimeout), timeStamp);
- }
- public boolean degreeMode() {
- return degreeModeFromFlags(mFlags);
- }
- public boolean longTimeout() {
- return longTimeoutFromFlags(mFlags);
- }
- /**
- * Return a ContentValues object representing the current data.
- */
- public ContentValues toContentValues() {
- ContentValues cvs = new ContentValues();
- cvs.put(ExpressionEntry.COLUMN_NAME_EXPRESSION, mExpression);
- cvs.put(ExpressionEntry.COLUMN_NAME_FLAGS, mFlags);
- if (mTimeStamp == 0) {
- mTimeStamp = System.currentTimeMillis();
- }
- cvs.put(ExpressionEntry.COLUMN_NAME_TIMESTAMP, mTimeStamp);
- return cvs;
- }
- }
-
- private static final String SQL_CREATE_ENTRIES =
- "CREATE TABLE " + ExpressionEntry.TABLE_NAME + " ("
- + ExpressionEntry._ID + " INTEGER PRIMARY KEY,"
- + ExpressionEntry.COLUMN_NAME_EXPRESSION + " BLOB,"
- + ExpressionEntry.COLUMN_NAME_FLAGS + " INTEGER,"
- + ExpressionEntry.COLUMN_NAME_TIMESTAMP + " INTEGER)";
- private static final String SQL_DROP_TABLE =
- "DROP TABLE IF EXISTS " + ExpressionEntry.TABLE_NAME;
- private static final String SQL_GET_MIN = "SELECT MIN(" + ExpressionEntry._ID
- + ") FROM " + ExpressionEntry.TABLE_NAME;
- private static final String SQL_GET_MAX = "SELECT MAX(" + ExpressionEntry._ID
- + ") FROM " + ExpressionEntry.TABLE_NAME;
- private static final String SQL_GET_ROW = "SELECT * FROM " + ExpressionEntry.TABLE_NAME
- + " WHERE " + ExpressionEntry._ID + " = ?";
- private static final String SQL_GET_ALL = "SELECT * FROM " + ExpressionEntry.TABLE_NAME
- + " WHERE " + ExpressionEntry._ID + " <= ? AND " +
- ExpressionEntry._ID + " >= ?" + " ORDER BY " + ExpressionEntry._ID + " DESC ";
- // We may eventually need an index by timestamp. We don't use it yet.
- private static final String SQL_CREATE_TIMESTAMP_INDEX =
- "CREATE INDEX timestamp_index ON " + ExpressionEntry.TABLE_NAME + "("
- + ExpressionEntry.COLUMN_NAME_TIMESTAMP + ")";
- private static final String SQL_DROP_TIMESTAMP_INDEX = "DROP INDEX IF EXISTS timestamp_index";
-
- private class ExpressionDBHelper extends SQLiteOpenHelper {
- // If you change the database schema, you must increment the database version.
- public static final int DATABASE_VERSION = 1;
- public static final String DATABASE_NAME = "Expressions.db";
-
- public ExpressionDBHelper(Context context) {
- super(context, DATABASE_NAME, null, DATABASE_VERSION);
- }
- public void onCreate(SQLiteDatabase db) {
- db.execSQL(SQL_CREATE_ENTRIES);
- db.execSQL(SQL_CREATE_TIMESTAMP_INDEX);
- }
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- // For now just throw away history on database version upgrade/downgrade.
- db.execSQL(SQL_DROP_TIMESTAMP_INDEX);
- db.execSQL(SQL_DROP_TABLE);
- onCreate(db);
- }
- public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- onUpgrade(db, oldVersion, newVersion);
- }
- }
-
- private ExpressionDBHelper mExpressionDBHelper;
-
- private SQLiteDatabase mExpressionDB; // Constant after initialization.
-
- // Expression indices between mMinAccessible and mMaxAccessible inclusive can be accessed.
- // We set these to more interesting values if a database access fails.
- // We punt on writes outside this range. We should never read outside this range.
- // If higher layers refer to an index outside this range, it will already be cached.
- // This also somewhat limits the size of the database, but only to an unreasonably
- // huge value.
- private long mMinAccessible = -10000000L;
- private long mMaxAccessible = 10000000L;
-
- // Never allocate new negative indicees (row ids) >= MAXIMUM_MIN_INDEX.
- public static final long MAXIMUM_MIN_INDEX = -10;
-
- // Minimum index value in DB.
- private long mMinIndex;
- // Maximum index value in DB.
- private long mMaxIndex;
-
- // A cursor that refers to the whole table, in reverse order.
- private AbstractWindowedCursor mAllCursor;
-
- // Expression index corresponding to a zero absolute offset for mAllCursor.
- // This is the argument we passed to the query.
- // We explicitly query only for entries that existed when we started, to avoid
- // interference from updates as we're running. It's unclear whether or not this matters.
- private int mAllCursorBase;
-
- // Database has been opened, mMinIndex and mMaxIndex are correct, mAllCursorBase and
- // mAllCursor have been set.
- private boolean mDBInitialized;
-
- // Gap between negative and positive row ids in the database.
- // Expressions with index [MAXIMUM_MIN_INDEX .. 0] are not stored.
- private static final long GAP = -MAXIMUM_MIN_INDEX + 1;
-
- // mLock protects mExpressionDB, mMinAccessible, and mMaxAccessible, mAllCursor,
- // mAllCursorBase, mMinIndex, mMaxIndex, and mDBInitialized. We access mExpressionDB without
- // synchronization after it's known to be initialized. Used to wait for database
- // initialization.
- private Object mLock = new Object();
-
- public ExpressionDB(Context context) {
- mExpressionDBHelper = new ExpressionDBHelper(context);
- AsyncInitializer initializer = new AsyncInitializer();
- // All calls that create background database accesses are made from the UI thread, and
- // use a SERIAL_EXECUTOR. Thus they execute in order.
- initializer.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, mExpressionDBHelper);
- }
-
- // Is database completely unusable?
- private boolean isDBBad() {
- if (!CONTINUE_WITH_BAD_DB) {
- return false;
- }
- synchronized(mLock) {
- return mMinAccessible > mMaxAccessible;
- }
- }
-
- // Is the index in the accessible range of the database?
- private boolean inAccessibleRange(long index) {
- if (!CONTINUE_WITH_BAD_DB) {
- return true;
- }
- synchronized(mLock) {
- return index >= mMinAccessible && index <= mMaxAccessible;
- }
- }
-
-
- private void setBadDB() {
- if (!CONTINUE_WITH_BAD_DB) {
- Log.e("Calculator", "Database access failed");
- throw new RuntimeException("Database access failed");
- }
- displayDatabaseWarning();
- synchronized(mLock) {
- mMinAccessible = 1L;
- mMaxAccessible = -1L;
- }
- }
-
- /**
- * Initialize the database in the background.
- */
- private class AsyncInitializer extends AsyncTask<ExpressionDBHelper, Void, SQLiteDatabase> {
- @Override
- protected SQLiteDatabase doInBackground(ExpressionDBHelper... helper) {
- try {
- SQLiteDatabase db = helper[0].getWritableDatabase();
- synchronized(mLock) {
- mExpressionDB = db;
- try (Cursor minResult = db.rawQuery(SQL_GET_MIN, null)) {
- if (!minResult.moveToFirst()) {
- // Empty database.
- mMinIndex = MAXIMUM_MIN_INDEX;
- } else {
- mMinIndex = Math.min(minResult.getLong(0), MAXIMUM_MIN_INDEX);
- }
- }
- try (Cursor maxResult = db.rawQuery(SQL_GET_MAX, null)) {
- if (!maxResult.moveToFirst()) {
- // Empty database.
- mMaxIndex = 0L;
- } else {
- mMaxIndex = Math.max(maxResult.getLong(0), 0L);
- }
- }
- if (mMaxIndex > Integer.MAX_VALUE) {
- throw new AssertionError("Expression index absurdly large");
- }
- mAllCursorBase = (int)mMaxIndex;
- if (mMaxIndex != 0L || mMinIndex != MAXIMUM_MIN_INDEX) {
- // Set up a cursor for reading the entire database.
- String args[] = new String[]
- { Long.toString(mAllCursorBase), Long.toString(mMinIndex) };
- mAllCursor = (AbstractWindowedCursor) db.rawQuery(SQL_GET_ALL, args);
- if (!mAllCursor.moveToFirst()) {
- setBadDB();
- return null;
- }
- }
- mDBInitialized = true;
- // We notify here, since there are unlikely cases in which the UI thread
- // may be blocked on us, preventing onPostExecute from running.
- mLock.notifyAll();
- }
- return db;
- } catch(SQLiteException e) {
- Log.e("Calculator", "Database initialization failed.\n", e);
- synchronized(mLock) {
- setBadDB();
- mLock.notifyAll();
- }
- return null;
- }
- }
-
- @Override
- protected void onPostExecute(SQLiteDatabase result) {
- if (result == null) {
- displayDatabaseWarning();
- } // else doInBackground already set expressionDB.
- }
- // On cancellation we do nothing;
- }
-
- private boolean databaseWarningIssued;
-
- /**
- * Display a warning message that a database access failed.
- * Do this only once. TODO: Replace with a real UI message.
- */
- void displayDatabaseWarning() {
- if (!databaseWarningIssued) {
- Log.e("Calculator", "Calculator restarting due to database error");
- databaseWarningIssued = true;
- }
- }
-
- /**
- * Wait until the database and mAllCursor, etc. have been initialized.
- */
- private void waitForDBInitialized() {
- synchronized(mLock) {
- // InterruptedExceptions are inconvenient here. Defer.
- boolean caught = false;
- while (!mDBInitialized && !isDBBad()) {
- try {
- mLock.wait();
- } catch(InterruptedException e) {
- caught = true;
- }
- }
- if (caught) {
- Thread.currentThread().interrupt();
- }
- }
- }
-
- /**
- * Erase the entire database. Assumes no other accesses to the database are
- * currently in progress
- * These tasks must be executed on a serial executor to avoid reordering writes.
- */
- private class AsyncEraser extends AsyncTask<Void, Void, Void> {
- @Override
- protected Void doInBackground(Void... nothings) {
- mExpressionDB.execSQL(SQL_DROP_TIMESTAMP_INDEX);
- mExpressionDB.execSQL(SQL_DROP_TABLE);
- try {
- mExpressionDB.execSQL("VACUUM");
- } catch(Exception e) {
- Log.v("Calculator", "Database VACUUM failed\n", e);
- // Should only happen with concurrent execution, which should be impossible.
- }
- mExpressionDB.execSQL(SQL_CREATE_ENTRIES);
- mExpressionDB.execSQL(SQL_CREATE_TIMESTAMP_INDEX);
- return null;
- }
- @Override
- protected void onPostExecute(Void nothing) {
- synchronized(mLock) {
- // Reinitialize everything to an empty and fully functional database.
- mMinAccessible = -10000000L;
- mMaxAccessible = 10000000L;
- mMinIndex = MAXIMUM_MIN_INDEX;
- mMaxIndex = mAllCursorBase = 0;
- mDBInitialized = true;
- mLock.notifyAll();
- }
- }
- // On cancellation we do nothing;
- }
-
- /**
- * Erase ALL database entries.
- * This is currently only safe if expressions that may refer to them are also erased.
- * Should only be called when concurrent references to the database are impossible.
- * TODO: Look at ways to more selectively clear the database.
- */
- public void eraseAll() {
- waitForDBInitialized();
- synchronized(mLock) {
- mDBInitialized = false;
- }
- AsyncEraser eraser = new AsyncEraser();
- eraser.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
- }
-
- // We track the number of outstanding writes to prevent onSaveInstanceState from
- // completing with in-flight database writes.
-
- private int mIncompleteWrites = 0;
- private Object mWriteCountsLock = new Object(); // Protects the preceding field.
-
- private void writeCompleted() {
- synchronized(mWriteCountsLock) {
- if (--mIncompleteWrites == 0) {
- mWriteCountsLock.notifyAll();
- }
- }
- }
-
- private void writeStarted() {
- synchronized(mWriteCountsLock) {
- ++mIncompleteWrites;
- }
- }
-
- /**
- * Wait for in-flight writes to complete.
- * This is not safe to call from one of our background tasks, since the writing
- * tasks may be waiting for the same underlying thread that we're using, resulting
- * in deadlock.
- */
- public void waitForWrites() {
- synchronized(mWriteCountsLock) {
- boolean caught = false;
- while (mIncompleteWrites != 0) {
- try {
- mWriteCountsLock.wait();
- } catch (InterruptedException e) {
- caught = true;
- }
- }
- if (caught) {
- Thread.currentThread().interrupt();
- }
- }
- }
-
- /**
- * Insert the given row in the database without blocking the UI thread.
- * These tasks must be executed on a serial executor to avoid reordering writes.
- */
- private class AsyncWriter extends AsyncTask<ContentValues, Void, Long> {
- @Override
- protected Long doInBackground(ContentValues... cvs) {
- long index = cvs[0].getAsLong(ExpressionEntry._ID);
- long result = mExpressionDB.insert(ExpressionEntry.TABLE_NAME, null, cvs[0]);
- writeCompleted();
- // Return 0 on success, row id on failure.
- if (result == -1) {
- return index;
- } else if (result != index) {
- throw new AssertionError("Expected row id " + index + ", got " + result);
- } else {
- return 0L;
- }
- }
- @Override
- protected void onPostExecute(Long result) {
- if (result != 0) {
- synchronized(mLock) {
- if (result > 0) {
- mMaxAccessible = result - 1;
- } else {
- mMinAccessible = result + 1;
- }
- }
- displayDatabaseWarning();
- }
- }
- // On cancellation we do nothing;
- }
-
- /**
- * Add a row with index outside existing range.
- * The returned index will be just larger than any existing index unless negative_index is true.
- * In that case it will be smaller than any existing index and smaller than MAXIMUM_MIN_INDEX.
- * This ensures that prior additions have completed, but does not wait for this insertion
- * to complete.
- */
- public long addRow(boolean negativeIndex, RowData data) {
- long result;
- long newIndex;
- waitForDBInitialized();
- synchronized(mLock) {
- if (negativeIndex) {
- newIndex = mMinIndex - 1;
- mMinIndex = newIndex;
- } else {
- newIndex = mMaxIndex + 1;
- mMaxIndex = newIndex;
- }
- if (!inAccessibleRange(newIndex)) {
- // Just drop it, but go ahead and return a new index to use for the cache.
- // So long as reads of previously written expressions continue to work,
- // we should be fine. When the application is restarted, history will revert
- // to just include values between mMinAccessible and mMaxAccessible.
- return newIndex;
- }
- writeStarted();
- ContentValues cvs = data.toContentValues();
- cvs.put(ExpressionEntry._ID, newIndex);
- AsyncWriter awriter = new AsyncWriter();
- // Ensure that writes are executed in order.
- awriter.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, cvs);
- }
- return newIndex;
- }
-
- /**
- * Generate a fake database row that's good enough to hopefully prevent crashes,
- * but bad enough to avoid confusion with real data. In particular, the result
- * will fail to evaluate.
- */
- RowData makeBadRow() {
- CalculatorExpr badExpr = new CalculatorExpr();
- badExpr.add(R.id.lparen);
- badExpr.add(R.id.rparen);
- return new RowData(badExpr.toBytes(), false, false, 0);
- }
-
- /**
- * Retrieve the row with the given index using a direct query.
- * Such a row must exist.
- * We assume that the database has been initialized, and the argument has been range checked.
- */
- private RowData getRowDirect(long index) {
- RowData result;
- String args[] = new String[] { Long.toString(index) };
- try (Cursor resultC = mExpressionDB.rawQuery(SQL_GET_ROW, args)) {
- if (!resultC.moveToFirst()) {
- setBadDB();
- return makeBadRow();
- } else {
- result = new RowData(resultC.getBlob(1), resultC.getInt(2) /* flags */,
- resultC.getLong(3) /* timestamp */);
- }
- }
- return result;
- }
-
- /**
- * Retrieve the row at the given offset from mAllCursorBase.
- * Note the argument is NOT an expression index!
- * We assume that the database has been initialized, and the argument has been range checked.
- */
- private RowData getRowFromCursor(int offset) {
- RowData result;
- synchronized(mLock) {
- if (!mAllCursor.moveToPosition(offset)) {
- Log.e("Calculator", "Failed to move cursor to position " + offset);
- setBadDB();
- return makeBadRow();
- }
- return new RowData(mAllCursor.getBlob(1), mAllCursor.getInt(2) /* flags */,
- mAllCursor.getLong(3) /* timestamp */);
- }
- }
-
- /**
- * Retrieve the database row at the given index.
- * We currently assume that we never read data that we added since we initialized the database.
- * This makes sense, since we cache it anyway. And we should always cache recently added data.
- */
- public RowData getRow(long index) {
- waitForDBInitialized();
- if (!inAccessibleRange(index)) {
- // Even if something went wrong opening or writing the database, we should
- // not see such read requests, unless they correspond to a persistently
- // saved index, and we can't retrieve that expression.
- displayDatabaseWarning();
- return makeBadRow();
- }
- int position = mAllCursorBase - (int)index;
- // We currently assume that the only gap between expression indices is the one around 0.
- if (index < 0) {
- position -= GAP;
- }
- if (position < 0) {
- throw new AssertionError("Database access out of range, index = " + index
- + " rel. pos. = " + position);
- }
- if (index < 0) {
- // Avoid using mAllCursor to read data that's far away from the current position,
- // since we're likely to have to return to the current position.
- // This is a heuristic; we don't worry about doing the "wrong" thing in the race case.
- int endPosition;
- synchronized(mLock) {
- CursorWindow window = mAllCursor.getWindow();
- endPosition = window.getStartPosition() + window.getNumRows();
- }
- if (position >= endPosition) {
- return getRowDirect(index);
- }
- }
- // In the positive index case, it's probably OK to cross a cursor boundary, since
- // we're much more likely to stay in the new window.
- return getRowFromCursor(position);
- }
-
- public long getMinIndex() {
- waitForDBInitialized();
- synchronized(mLock) {
- return mMinIndex;
- }
- }
-
- public long getMaxIndex() {
- waitForDBInitialized();
- synchronized(mLock) {
- return mMaxIndex;
- }
- }
-
- public void close() {
- mExpressionDBHelper.close();
- }
-
-}
diff --git a/src/com/android/calculator2/HistoryAdapter.java b/src/com/android/calculator2/HistoryAdapter.java
deleted file mode 100644
index be50050..0000000
--- a/src/com/android/calculator2/HistoryAdapter.java
+++ /dev/null
@@ -1,222 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.calculator2;
-
-import androidx.recyclerview.widget.RecyclerView;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.List;
-
-/**
- * Adapter for RecyclerView of HistoryItems.
- */
-public class HistoryAdapter extends RecyclerView.Adapter<HistoryAdapter.ViewHolder> {
-
- private static final String TAG = "HistoryAdapter";
-
- private static final int EMPTY_VIEW_TYPE = 0;
- public static final int HISTORY_VIEW_TYPE = 1;
-
- private Evaluator mEvaluator;
-
- private final Calendar mCalendar = Calendar.getInstance();
-
- private List<HistoryItem> mDataSet;
-
- private boolean mIsResultLayout;
- private boolean mIsOneLine;
- private boolean mIsDisplayEmpty;
-
- public HistoryAdapter(ArrayList<HistoryItem> dataSet) {
- mDataSet = dataSet;
- setHasStableIds(true);
- }
-
- @Override
- public HistoryAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- final View v;
- if (viewType == HISTORY_VIEW_TYPE) {
- v = LayoutInflater.from(parent.getContext())
- .inflate(R.layout.history_item, parent, false);
- } else {
- v = LayoutInflater.from(parent.getContext())
- .inflate(R.layout.empty_history_view, parent, false);
- }
- return new ViewHolder(v, viewType);
- }
-
- @Override
- public void onBindViewHolder(final HistoryAdapter.ViewHolder holder, int position) {
- final HistoryItem item = getItem(position);
-
- if (item.isEmptyView()) {
- return;
- }
-
- holder.mFormula.setText(item.getFormula());
- // Note: HistoryItems that are not the current expression will always have interesting ops.
- holder.mResult.setEvaluator(mEvaluator, item.getEvaluatorIndex());
- if (item.getEvaluatorIndex() == Evaluator.HISTORY_MAIN_INDEX) {
- holder.mDate.setText(R.string.title_current_expression);
- holder.mResult.setVisibility(mIsOneLine ? View.GONE : View.VISIBLE);
- } else {
- // If the previous item occurred on the same date, the current item does not need
- // a date header.
- if (shouldShowHeader(position, item)) {
- holder.mDate.setText(item.getDateString());
- // Special case -- very first item should not have a divider above it.
- holder.mDivider.setVisibility(position == getItemCount() - 1
- ? View.GONE : View.VISIBLE);
- } else {
- holder.mDate.setVisibility(View.GONE);
- holder.mDivider.setVisibility(View.INVISIBLE);
- }
- }
- }
-
- @Override
- public void onViewRecycled(ViewHolder holder) {
- if (holder.getItemViewType() == EMPTY_VIEW_TYPE) {
- return;
- }
- mEvaluator.cancel(holder.getItemId(), true);
-
- holder.mDate.setVisibility(View.VISIBLE);
- holder.mDivider.setVisibility(View.VISIBLE);
- holder.mDate.setText(null);
- holder.mFormula.setText(null);
- holder.mResult.setText(null);
-
- super.onViewRecycled(holder);
- }
-
- @Override
- public long getItemId(int position) {
- return getItem(position).getEvaluatorIndex();
- }
-
- @Override
- public int getItemViewType(int position) {
- return getItem(position).isEmptyView() ? EMPTY_VIEW_TYPE : HISTORY_VIEW_TYPE;
- }
-
- @Override
- public int getItemCount() {
- return mDataSet.size();
- }
-
- public void setDataSet(ArrayList<HistoryItem> dataSet) {
- mDataSet = dataSet;
- }
-
- public void setIsResultLayout(boolean isResult) {
- mIsResultLayout = isResult;
- }
-
- public void setIsOneLine(boolean isOneLine) {
- mIsOneLine = isOneLine;
- }
-
- public void setIsDisplayEmpty(boolean isDisplayEmpty) {
- mIsDisplayEmpty = isDisplayEmpty;
- }
-
- public void setEvaluator(Evaluator evaluator) {
- mEvaluator = evaluator;
- }
-
- private int getEvaluatorIndex(int position) {
- if (mIsDisplayEmpty || mIsResultLayout) {
- return (int) (mEvaluator.getMaxIndex() - position);
- } else {
- // Account for the additional "Current Expression" with the +1.
- return (int) (mEvaluator.getMaxIndex() - position + 1);
- }
- }
-
- private boolean shouldShowHeader(int position, HistoryItem item) {
- if (position == getItemCount() - 1) {
- // First/oldest element should always show the header.
- return true;
- }
- final HistoryItem prevItem = getItem(position + 1);
- // We need to use Calendars to determine this because of Daylight Savings.
- mCalendar.setTimeInMillis(item.getTimeInMillis());
- final int year = mCalendar.get(Calendar.YEAR);
- final int day = mCalendar.get(Calendar.DAY_OF_YEAR);
- mCalendar.setTimeInMillis(prevItem.getTimeInMillis());
- final int prevYear = mCalendar.get(Calendar.YEAR);
- final int prevDay = mCalendar.get(Calendar.DAY_OF_YEAR);
- return year != prevYear || day != prevDay;
- }
-
- /**
- * Gets the HistoryItem from mDataSet, lazy-filling the dataSet if necessary.
- */
- private HistoryItem getItem(int position) {
- HistoryItem item = mDataSet.get(position);
- // Lazy-fill the data set.
- if (item == null) {
- final int evaluatorIndex = getEvaluatorIndex(position);
- item = new HistoryItem(evaluatorIndex,
- mEvaluator.getTimeStamp(evaluatorIndex),
- mEvaluator.getExprAsSpannable(evaluatorIndex));
- mDataSet.set(position, item);
- }
- return item;
- }
-
- public static class ViewHolder extends RecyclerView.ViewHolder {
-
- private TextView mDate;
- private AlignedTextView mFormula;
- private CalculatorResult mResult;
- private View mDivider;
-
- public ViewHolder(View v, int viewType) {
- super(v);
- if (viewType == EMPTY_VIEW_TYPE) {
- return;
- }
- mDate = (TextView) v.findViewById(R.id.history_date);
- mFormula = (AlignedTextView) v.findViewById(R.id.history_formula);
- mResult = (CalculatorResult) v.findViewById(R.id.history_result);
- mDivider = v.findViewById(R.id.history_divider);
- }
-
- public AlignedTextView getFormula() {
- return mFormula;
- }
-
- public CalculatorResult getResult() {
- return mResult;
- }
-
- public TextView getDate() {
- return mDate;
- }
-
- public View getDivider() {
- return mDivider;
- }
- }
-} \ No newline at end of file
diff --git a/src/com/android/calculator2/HistoryFragment.java b/src/com/android/calculator2/HistoryFragment.java
deleted file mode 100644
index eb2a325..0000000
--- a/src/com/android/calculator2/HistoryFragment.java
+++ /dev/null
@@ -1,243 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.calculator2;
-
-import android.animation.Animator;
-import android.app.Fragment;
-import android.os.Bundle;
-import androidx.core.content.ContextCompat;
-import androidx.recyclerview.widget.RecyclerView;
-import android.view.LayoutInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Toolbar;
-
-import java.util.ArrayList;
-
-import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_DRAGGING;
-
-public class HistoryFragment extends Fragment implements DragLayout.DragCallback {
-
- public static final String TAG = "HistoryFragment";
- public static final String CLEAR_DIALOG_TAG = "clear";
-
- private final DragController mDragController = new DragController();
-
- private RecyclerView mRecyclerView;
- private HistoryAdapter mAdapter;
- private DragLayout mDragLayout;
-
- private Evaluator mEvaluator;
-
- private ArrayList<HistoryItem> mDataSet = new ArrayList<>();
-
- private boolean mIsDisplayEmpty;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mAdapter = new HistoryAdapter(mDataSet);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- final View view = inflater.inflate(
- R.layout.fragment_history, container, false /* attachToRoot */);
-
- mDragLayout = (DragLayout) container.getRootView().findViewById(R.id.drag_layout);
- mDragLayout.addDragCallback(this);
-
- mRecyclerView = (RecyclerView) view.findViewById(R.id.history_recycler_view);
- mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
- @Override
- public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
- if (newState == SCROLL_STATE_DRAGGING) {
- stopActionModeOrContextMenu();
- }
- super.onScrollStateChanged(recyclerView, newState);
- }
- });
-
- // The size of the RecyclerView is not affected by the adapter's contents.
- mRecyclerView.setHasFixedSize(true);
- mRecyclerView.setAdapter(mAdapter);
-
- final Toolbar toolbar = (Toolbar) view.findViewById(R.id.history_toolbar);
- toolbar.inflateMenu(R.menu.fragment_history);
- toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
- @Override
- public boolean onMenuItemClick(MenuItem item) {
- if (item.getItemId() == R.id.menu_clear_history) {
- final Calculator calculator = (Calculator) getActivity();
- AlertDialogFragment.showMessageDialog(calculator, "" /* title */,
- getString(R.string.dialog_clear),
- getString(R.string.menu_clear_history),
- CLEAR_DIALOG_TAG);
- return true;
- }
- return onOptionsItemSelected(item);
- }
- });
- toolbar.setNavigationOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- getActivity().onBackPressed();
- }
- });
- return view;
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
-
- final Calculator activity = (Calculator) getActivity();
- mEvaluator = Evaluator.getInstance(activity);
- mAdapter.setEvaluator(mEvaluator);
-
- final boolean isResultLayout = activity.isResultLayout();
- final boolean isOneLine = activity.isOneLine();
-
- // Snapshot display state here. For the rest of the lifecycle of this current
- // HistoryFragment, this is what we will consider the display state.
- // In rare cases, the display state can change after our adapter is initialized.
- final CalculatorExpr mainExpr = mEvaluator.getExpr(Evaluator.MAIN_INDEX);
- mIsDisplayEmpty = mainExpr == null || mainExpr.isEmpty();
-
- initializeController(isResultLayout, isOneLine, mIsDisplayEmpty);
-
- final long maxIndex = mEvaluator.getMaxIndex();
-
- final ArrayList<HistoryItem> newDataSet = new ArrayList<>();
-
- if (!mIsDisplayEmpty && !isResultLayout) {
- // Add the current expression as the first element in the list (the layout is
- // reversed and we want the current expression to be the last one in the
- // RecyclerView).
- // If we are in the result state, the result will animate to the last history
- // element in the list and there will be no "Current Expression."
- mEvaluator.copyMainToHistory();
- newDataSet.add(new HistoryItem(Evaluator.HISTORY_MAIN_INDEX,
- System.currentTimeMillis(), mEvaluator.getExprAsSpannable(0)));
- }
- for (long i = 0; i < maxIndex; ++i) {
- newDataSet.add(null);
- }
- final boolean isEmpty = newDataSet.isEmpty();
- mRecyclerView.setBackgroundColor(ContextCompat.getColor(activity,
- isEmpty ? R.color.empty_history_color : R.color.display_background_color));
- if (isEmpty) {
- newDataSet.add(new HistoryItem());
- }
- mDataSet = newDataSet;
- mAdapter.setDataSet(mDataSet);
- mAdapter.setIsResultLayout(isResultLayout);
- mAdapter.setIsOneLine(activity.isOneLine());
- mAdapter.setIsDisplayEmpty(mIsDisplayEmpty);
- mAdapter.notifyDataSetChanged();
- }
-
- @Override
- public void onStart() {
- super.onStart();
-
- final Calculator activity = (Calculator) getActivity();
- mDragController.initializeAnimation(activity.isResultLayout(), activity.isOneLine(),
- mIsDisplayEmpty);
- }
-
- @Override
- public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) {
- return mDragLayout.createAnimator(enter);
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
-
- if (mDragLayout != null) {
- mDragLayout.removeDragCallback(this);
- }
-
- if (mEvaluator != null) {
- // Note that the view is destroyed when the fragment backstack is popped, so
- // these are essentially called when the DragLayout is closed.
- mEvaluator.cancelNonMain();
- }
- }
-
- private void initializeController(boolean isResult, boolean isOneLine, boolean isDisplayEmpty) {
- mDragController.setDisplayFormula(
- (CalculatorFormula) getActivity().findViewById(R.id.formula));
- mDragController.setDisplayResult(
- (CalculatorResult) getActivity().findViewById(R.id.result));
- mDragController.setToolbar(getActivity().findViewById(R.id.toolbar));
- mDragController.setEvaluator(mEvaluator);
- mDragController.initializeController(isResult, isOneLine, isDisplayEmpty);
- }
-
- public boolean stopActionModeOrContextMenu() {
- if (mRecyclerView == null) {
- return false;
- }
- for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
- final View view = mRecyclerView.getChildAt(i);
- final HistoryAdapter.ViewHolder viewHolder =
- (HistoryAdapter.ViewHolder) mRecyclerView.getChildViewHolder(view);
- if (viewHolder != null && viewHolder.getResult() != null
- && viewHolder.getResult().stopActionModeOrContextMenu()) {
- return true;
- }
- }
- return false;
- }
-
- /* Begin override DragCallback methods. */
-
- @Override
- public void onStartDraggingOpen() {
- // no-op
- }
-
- @Override
- public void onInstanceStateRestored(boolean isOpen) {
- if (isOpen) {
- mRecyclerView.setVisibility(View.VISIBLE);
- }
- }
-
- @Override
- public void whileDragging(float yFraction) {
- if (isVisible() || isRemoving()) {
- mDragController.animateViews(yFraction, mRecyclerView);
- }
- }
-
- @Override
- public boolean shouldCaptureView(View view, int x, int y) {
- return !mRecyclerView.canScrollVertically(1 /* scrolling down */);
- }
-
- @Override
- public int getDisplayHeight() {
- return 0;
- }
-
- /* End override DragCallback methods. */
-}
diff --git a/src/com/android/calculator2/HistoryItem.java b/src/com/android/calculator2/HistoryItem.java
deleted file mode 100644
index f20d1a7..0000000
--- a/src/com/android/calculator2/HistoryItem.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.calculator2;
-
-import android.text.Spannable;
-import android.text.format.DateUtils;
-
-public class HistoryItem {
-
- private long mEvaluatorIndex;
- /** Date in millis */
- private long mTimeInMillis;
- private Spannable mFormula;
-
- /** This is true only for the "empty history" view. */
- private final boolean mIsEmpty;
-
- public HistoryItem(long evaluatorIndex, long millis, Spannable formula) {
- mEvaluatorIndex = evaluatorIndex;
- mTimeInMillis = millis;
- mFormula = formula;
- mIsEmpty = false;
- }
-
- public long getEvaluatorIndex() {
- return mEvaluatorIndex;
- }
-
- public HistoryItem() {
- mIsEmpty = true;
- }
-
- public boolean isEmptyView() {
- return mIsEmpty;
- }
-
- /**
- * @return String in format "n days ago"
- * For n > 7, the date is returned.
- */
- public CharSequence getDateString() {
- return DateUtils.getRelativeTimeSpanString(mTimeInMillis, System.currentTimeMillis(),
- DateUtils.DAY_IN_MILLIS, DateUtils.FORMAT_ABBREV_RELATIVE);
- }
-
- public long getTimeInMillis() {
- return mTimeInMillis;
- }
-
- public Spannable getFormula() {
- return mFormula;
- }
-} \ No newline at end of file
diff --git a/src/com/android/calculator2/KeyMaps.java b/src/com/android/calculator2/KeyMaps.java
deleted file mode 100644
index cdfe4e4..0000000
--- a/src/com/android/calculator2/KeyMaps.java
+++ /dev/null
@@ -1,683 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.calculator2;
-
-import android.content.res.Resources;
-import android.content.Context;
-import android.app.Activity;
-import android.util.Log;
-import android.view.View;
-import android.widget.Button;
-
-import java.text.DecimalFormatSymbols;
-import java.util.HashMap;
-import java.util.Locale;
-
-/**
- * Collection of mapping functions between key ids, characters, internationalized
- * and non-internationalized characters, etc.
- * <p>
- * KeyMap instances are not meaningful; everything here is static.
- * All functions are either pure, or are assumed to be called only from a single UI thread.
- */
-public class KeyMaps {
- /**
- * Map key id to corresponding (internationalized) display string.
- * Pure function.
- */
- public static String toString(Context context, int id) {
- switch(id) {
- case R.id.const_pi:
- return context.getString(R.string.const_pi);
- case R.id.const_e:
- return context.getString(R.string.const_e);
- case R.id.op_sqrt:
- return context.getString(R.string.op_sqrt);
- case R.id.op_fact:
- return context.getString(R.string.op_fact);
- case R.id.op_pct:
- return context.getString(R.string.op_pct);
- case R.id.fun_sin:
- return context.getString(R.string.fun_sin) + context.getString(R.string.lparen);
- case R.id.fun_cos:
- return context.getString(R.string.fun_cos) + context.getString(R.string.lparen);
- case R.id.fun_tan:
- return context.getString(R.string.fun_tan) + context.getString(R.string.lparen);
- case R.id.fun_arcsin:
- return context.getString(R.string.fun_arcsin) + context.getString(R.string.lparen);
- case R.id.fun_arccos:
- return context.getString(R.string.fun_arccos) + context.getString(R.string.lparen);
- case R.id.fun_arctan:
- return context.getString(R.string.fun_arctan) + context.getString(R.string.lparen);
- case R.id.fun_ln:
- return context.getString(R.string.fun_ln) + context.getString(R.string.lparen);
- case R.id.fun_log:
- return context.getString(R.string.fun_log) + context.getString(R.string.lparen);
- case R.id.fun_exp:
- // Button label doesn't work.
- return context.getString(R.string.exponential) + context.getString(R.string.lparen);
- case R.id.lparen:
- return context.getString(R.string.lparen);
- case R.id.rparen:
- return context.getString(R.string.rparen);
- case R.id.op_pow:
- return context.getString(R.string.op_pow);
- case R.id.op_mul:
- return context.getString(R.string.op_mul);
- case R.id.op_div:
- return context.getString(R.string.op_div);
- case R.id.op_add:
- return context.getString(R.string.op_add);
- case R.id.op_sub:
- return context.getString(R.string.op_sub);
- case R.id.op_sqr:
- // Button label doesn't work.
- return context.getString(R.string.squared);
- case R.id.dec_point:
- return context.getString(R.string.dec_point);
- case R.id.digit_0:
- return context.getString(R.string.digit_0);
- case R.id.digit_1:
- return context.getString(R.string.digit_1);
- case R.id.digit_2:
- return context.getString(R.string.digit_2);
- case R.id.digit_3:
- return context.getString(R.string.digit_3);
- case R.id.digit_4:
- return context.getString(R.string.digit_4);
- case R.id.digit_5:
- return context.getString(R.string.digit_5);
- case R.id.digit_6:
- return context.getString(R.string.digit_6);
- case R.id.digit_7:
- return context.getString(R.string.digit_7);
- case R.id.digit_8:
- return context.getString(R.string.digit_8);
- case R.id.digit_9:
- return context.getString(R.string.digit_9);
- default:
- return "";
- }
- }
-
- /**
- * Map key id to a single byte, somewhat human readable, description.
- * Used to serialize expressions in the database.
- * The result is in the range 0x20-0x7f.
- */
- public static byte toByte(int id) {
- char result;
- // We only use characters with single-byte UTF8 encodings in the range 0x20-0x7F.
- switch(id) {
- case R.id.const_pi:
- result = 'p';
- break;
- case R.id.const_e:
- result = 'e';
- break;
- case R.id.op_sqrt:
- result = 'r';
- break;
- case R.id.op_fact:
- result = '!';
- break;
- case R.id.op_pct:
- result = '%';
- break;
- case R.id.fun_sin:
- result = 's';
- break;
- case R.id.fun_cos:
- result = 'c';
- break;
- case R.id.fun_tan:
- result = 't';
- break;
- case R.id.fun_arcsin:
- result = 'S';
- break;
- case R.id.fun_arccos:
- result = 'C';
- break;
- case R.id.fun_arctan:
- result = 'T';
- break;
- case R.id.fun_ln:
- result = 'l';
- break;
- case R.id.fun_log:
- result = 'L';
- break;
- case R.id.fun_exp:
- result = 'E';
- break;
- case R.id.lparen:
- result = '(';
- break;
- case R.id.rparen:
- result = ')';
- break;
- case R.id.op_pow:
- result = '^';
- break;
- case R.id.op_mul:
- result = '*';
- break;
- case R.id.op_div:
- result = '/';
- break;
- case R.id.op_add:
- result = '+';
- break;
- case R.id.op_sub:
- result = '-';
- break;
- case R.id.op_sqr:
- result = '2';
- break;
- default:
- throw new AssertionError("Unexpected key id");
- }
- return (byte)result;
- }
-
- /**
- * Map single byte encoding generated by key id generated by toByte back to
- * key id.
- */
- public static int fromByte(byte b) {
- switch((char)b) {
- case 'p':
- return R.id.const_pi;
- case 'e':
- return R.id.const_e;
- case 'r':
- return R.id.op_sqrt;
- case '!':
- return R.id.op_fact;
- case '%':
- return R.id.op_pct;
- case 's':
- return R.id.fun_sin;
- case 'c':
- return R.id.fun_cos;
- case 't':
- return R.id.fun_tan;
- case 'S':
- return R.id.fun_arcsin;
- case 'C':
- return R.id.fun_arccos;
- case 'T':
- return R.id.fun_arctan;
- case 'l':
- return R.id.fun_ln;
- case 'L':
- return R.id.fun_log;
- case 'E':
- return R.id.fun_exp;
- case '(':
- return R.id.lparen;
- case ')':
- return R.id.rparen;
- case '^':
- return R.id.op_pow;
- case '*':
- return R.id.op_mul;
- case '/':
- return R.id.op_div;
- case '+':
- return R.id.op_add;
- case '-':
- return R.id.op_sub;
- case '2':
- return R.id.op_sqr;
- default:
- throw new AssertionError("Unexpected single byte operator encoding");
- }
- }
-
- /**
- * Map key id to corresponding (internationalized) descriptive string that can be used
- * to correctly read back a formula.
- * Only used for operators and individual characters; not used inside constants.
- * Returns null when we don't need a descriptive string.
- * Pure function.
- */
- public static String toDescriptiveString(Context context, int id) {
- switch(id) {
- case R.id.op_fact:
- return context.getString(R.string.desc_op_fact);
- case R.id.fun_sin:
- return context.getString(R.string.desc_fun_sin)
- + " " + context.getString(R.string.desc_lparen);
- case R.id.fun_cos:
- return context.getString(R.string.desc_fun_cos)
- + " " + context.getString(R.string.desc_lparen);
- case R.id.fun_tan:
- return context.getString(R.string.desc_fun_tan)
- + " " + context.getString(R.string.desc_lparen);
- case R.id.fun_arcsin:
- return context.getString(R.string.desc_fun_arcsin)
- + " " + context.getString(R.string.desc_lparen);
- case R.id.fun_arccos:
- return context.getString(R.string.desc_fun_arccos)
- + " " + context.getString(R.string.desc_lparen);
- case R.id.fun_arctan:
- return context.getString(R.string.desc_fun_arctan)
- + " " + context.getString(R.string.desc_lparen);
- case R.id.fun_ln:
- return context.getString(R.string.desc_fun_ln)
- + " " + context.getString(R.string.desc_lparen);
- case R.id.fun_log:
- return context.getString(R.string.desc_fun_log)
- + " " + context.getString(R.string.desc_lparen);
- case R.id.fun_exp:
- return context.getString(R.string.desc_fun_exp)
- + " " + context.getString(R.string.desc_lparen);
- case R.id.lparen:
- return context.getString(R.string.desc_lparen);
- case R.id.rparen:
- return context.getString(R.string.desc_rparen);
- case R.id.op_pow:
- return context.getString(R.string.desc_op_pow);
- case R.id.dec_point:
- return context.getString(R.string.desc_dec_point);
- default:
- return null;
- }
- }
-
- /**
- * Does a button id correspond to a binary operator?
- * Pure function.
- */
- public static boolean isBinary(int id) {
- switch(id) {
- case R.id.op_pow:
- case R.id.op_mul:
- case R.id.op_div:
- case R.id.op_add:
- case R.id.op_sub:
- return true;
- default:
- return false;
- }
- }
-
- /**
- * Does a button id correspond to a trig function?
- * Pure function.
- */
- public static boolean isTrigFunc(int id) {
- switch(id) {
- case R.id.fun_sin:
- case R.id.fun_cos:
- case R.id.fun_tan:
- case R.id.fun_arcsin:
- case R.id.fun_arccos:
- case R.id.fun_arctan:
- return true;
- default:
- return false;
- }
- }
-
- /**
- * Does a button id correspond to a function that introduces an implicit lparen?
- * Pure function.
- */
- public static boolean isFunc(int id) {
- if (isTrigFunc(id)) {
- return true;
- }
- switch(id) {
- case R.id.fun_ln:
- case R.id.fun_log:
- case R.id.fun_exp:
- return true;
- default:
- return false;
- }
- }
-
- /**
- * Does a button id correspond to a prefix operator?
- * Pure function.
- */
- public static boolean isPrefix(int id) {
- switch(id) {
- case R.id.op_sqrt:
- case R.id.op_sub:
- return true;
- default:
- return false;
- }
- }
-
- /**
- * Does a button id correspond to a suffix operator?
- */
- public static boolean isSuffix(int id) {
- switch (id) {
- case R.id.op_fact:
- case R.id.op_pct:
- case R.id.op_sqr:
- return true;
- default:
- return false;
- }
- }
-
- public static final int NOT_DIGIT = 10;
-
- public static final String ELLIPSIS = "\u2026";
-
- public static final char MINUS_SIGN = '\u2212';
-
- /**
- * Map key id to digit or NOT_DIGIT
- * Pure function.
- */
- public static int digVal(int id) {
- switch (id) {
- case R.id.digit_0:
- return 0;
- case R.id.digit_1:
- return 1;
- case R.id.digit_2:
- return 2;
- case R.id.digit_3:
- return 3;
- case R.id.digit_4:
- return 4;
- case R.id.digit_5:
- return 5;
- case R.id.digit_6:
- return 6;
- case R.id.digit_7:
- return 7;
- case R.id.digit_8:
- return 8;
- case R.id.digit_9:
- return 9;
- default:
- return NOT_DIGIT;
- }
- }
-
- /**
- * Map digit to corresponding key. Inverse of above.
- * Pure function.
- */
- public static int keyForDigVal(int v) {
- switch(v) {
- case 0:
- return R.id.digit_0;
- case 1:
- return R.id.digit_1;
- case 2:
- return R.id.digit_2;
- case 3:
- return R.id.digit_3;
- case 4:
- return R.id.digit_4;
- case 5:
- return R.id.digit_5;
- case 6:
- return R.id.digit_6;
- case 7:
- return R.id.digit_7;
- case 8:
- return R.id.digit_8;
- case 9:
- return R.id.digit_9;
- default:
- return View.NO_ID;
- }
- }
-
- // The following two are only used for recognizing additional
- // input characters from a physical keyboard. They are not used
- // for output internationalization.
- private static char mDecimalPt;
-
- private static char mPiChar;
-
- /**
- * Character used as a placeholder for digits that are currently unknown in a result that
- * is being computed. We initially generate blanks, and then use this as a replacement
- * during final translation.
- * <p/>
- * Note: the character must correspond closely to the width of a digit,
- * otherwise the UI will visibly shift once the computation is finished.
- */
- private static final char CHAR_DIGIT_UNKNOWN = '\u2007';
-
- /**
- * Map typed function name strings to corresponding button ids.
- * We (now redundantly?) include both localized and English names.
- */
- private static HashMap<String, Integer> sKeyValForFun;
-
- /**
- * Result string corresponding to a character in the calculator result.
- * The string values in the map are expected to be one character long.
- */
- private static HashMap<Character, String> sOutputForResultChar;
-
- /**
- * Locale corresponding to preceding map and character constants.
- * We recompute the map if this is not the current locale.
- */
- private static Locale sLocaleForMaps = null;
-
- /**
- * Activity to use for looking up buttons.
- */
- private static Activity mActivity;
-
- /**
- * Set acttivity used for looking up button labels.
- * Call only from UI thread.
- */
- public static void setActivity(Activity a) {
- mActivity = a;
- }
-
- /**
- * Return the button id corresponding to the supplied character or return NO_ID.
- * Called only by UI thread.
- */
- public static int keyForChar(char c) {
- validateMaps();
- if (Character.isDigit(c)) {
- int i = Character.digit(c, 10);
- return KeyMaps.keyForDigVal(i);
- }
- switch (c) {
- case '.':
- case ',':
- return R.id.dec_point;
- case '-':
- case MINUS_SIGN:
- return R.id.op_sub;
- case '+':
- return R.id.op_add;
- case '*':
- case '\u00D7': // MULTIPLICATION SIGN
- return R.id.op_mul;
- case '/':
- case '\u00F7': // DIVISION SIGN
- return R.id.op_div;
- // We no longer localize function names, so they can't start with an 'e' or 'p'.
- case 'e':
- case 'E':
- return R.id.const_e;
- case 'p':
- case 'P':
- return R.id.const_pi;
- case '^':
- return R.id.op_pow;
- case '!':
- return R.id.op_fact;
- case '%':
- return R.id.op_pct;
- case '(':
- return R.id.lparen;
- case ')':
- return R.id.rparen;
- default:
- if (c == mDecimalPt) return R.id.dec_point;
- if (c == mPiChar) return R.id.const_pi;
- // pi is not translated, but it might be typable on a Greek keyboard,
- // or pasted in, so we check ...
- return View.NO_ID;
- }
- }
-
- /**
- * Add information corresponding to the given button id to sKeyValForFun, to be used
- * when mapping keyboard input to button ids.
- */
- static void addButtonToFunMap(int button_id) {
- Button button = (Button)mActivity.findViewById(button_id);
- sKeyValForFun.put(button.getText().toString(), button_id);
- }
-
- /**
- * Add information corresponding to the given button to sOutputForResultChar, to be used
- * when translating numbers on output.
- */
- static void addButtonToOutputMap(char c, int button_id) {
- Button button = (Button)mActivity.findViewById(button_id);
- sOutputForResultChar.put(c, button.getText().toString());
- }
-
- /**
- * Ensure that the preceding map and character constants correspond to the current locale.
- * Called only by UI thread.
- */
- static void validateMaps() {
- Locale locale = Locale.getDefault();
- if (!locale.equals(sLocaleForMaps)) {
- Log.v ("Calculator", "Setting locale to: " + locale.toLanguageTag());
- sKeyValForFun = new HashMap<String, Integer>();
- sKeyValForFun.put("sin", R.id.fun_sin);
- sKeyValForFun.put("cos", R.id.fun_cos);
- sKeyValForFun.put("tan", R.id.fun_tan);
- sKeyValForFun.put("arcsin", R.id.fun_arcsin);
- sKeyValForFun.put("arccos", R.id.fun_arccos);
- sKeyValForFun.put("arctan", R.id.fun_arctan);
- sKeyValForFun.put("asin", R.id.fun_arcsin);
- sKeyValForFun.put("acos", R.id.fun_arccos);
- sKeyValForFun.put("atan", R.id.fun_arctan);
- sKeyValForFun.put("ln", R.id.fun_ln);
- sKeyValForFun.put("log", R.id.fun_log);
- sKeyValForFun.put("sqrt", R.id.op_sqrt); // special treatment
- addButtonToFunMap(R.id.fun_sin);
- addButtonToFunMap(R.id.fun_cos);
- addButtonToFunMap(R.id.fun_tan);
- addButtonToFunMap(R.id.fun_arcsin);
- addButtonToFunMap(R.id.fun_arccos);
- addButtonToFunMap(R.id.fun_arctan);
- addButtonToFunMap(R.id.fun_ln);
- addButtonToFunMap(R.id.fun_log);
-
- // Set locale-dependent character "constants"
- mDecimalPt =
- DecimalFormatSymbols.getInstance().getDecimalSeparator();
- // We recognize this in keyboard input, even if we use
- // a different character.
- Resources res = mActivity.getResources();
- mPiChar = 0;
- String piString = res.getString(R.string.const_pi);
- if (piString.length() == 1) {
- mPiChar = piString.charAt(0);
- }
-
- sOutputForResultChar = new HashMap<Character, String>();
- sOutputForResultChar.put('e', "E");
- sOutputForResultChar.put('E', "E");
- sOutputForResultChar.put(' ', String.valueOf(CHAR_DIGIT_UNKNOWN));
- sOutputForResultChar.put(ELLIPSIS.charAt(0), ELLIPSIS);
- // Translate numbers for fraction display, but not the separating slash, which appears
- // to be universal. We also do not translate the ln, sqrt, pi
- sOutputForResultChar.put('/', "/");
- sOutputForResultChar.put('(', "(");
- sOutputForResultChar.put(')', ")");
- sOutputForResultChar.put('l', "l");
- sOutputForResultChar.put('n', "n");
- sOutputForResultChar.put(',',
- String.valueOf(DecimalFormatSymbols.getInstance().getGroupingSeparator()));
- sOutputForResultChar.put('\u221A', "\u221A"); // SQUARE ROOT
- sOutputForResultChar.put('\u03C0', "\u03C0"); // GREEK SMALL LETTER PI
- addButtonToOutputMap('-', R.id.op_sub);
- addButtonToOutputMap('.', R.id.dec_point);
- for (int i = 0; i <= 9; ++i) {
- addButtonToOutputMap((char)('0' + i), keyForDigVal(i));
- }
-
- sLocaleForMaps = locale;
-
- }
- }
-
- /**
- * Return function button id for the substring of s starting at pos and ending with
- * the next "(". Return NO_ID if there is none.
- * We currently check for both (possibly localized) button labels, and standard
- * English names. (They should currently be the same, and hence this is currently redundant.)
- * Callable only from UI thread.
- */
- public static int funForString(String s, int pos) {
- validateMaps();
- int parenPos = s.indexOf('(', pos);
- if (parenPos != -1) {
- String funString = s.substring(pos, parenPos);
- Integer keyValue = sKeyValForFun.get(funString);
- if (keyValue == null) return View.NO_ID;
- return keyValue;
- }
- return View.NO_ID;
- }
-
- /**
- * Return the localization of the string s representing a numeric answer.
- * Callable only from UI thread.
- * A trailing e is treated as the mathematical constant, not an exponent.
- */
- public static String translateResult(String s) {
- StringBuilder result = new StringBuilder();
- int len = s.length();
- validateMaps();
- for (int i = 0; i < len; ++i) {
- char c = s.charAt(i);
- if (i < len - 1 || c != 'e') {
- String translation = sOutputForResultChar.get(c);
- if (translation == null) {
- // Should not get here. Report if we do.
- Log.v("Calculator", "Bad character:" + c);
- result.append(String.valueOf(c));
- } else {
- result.append(translation);
- }
- }
- }
- return result.toString();
- }
-
-}
diff --git a/src/com/android/calculator2/Licenses.java b/src/com/android/calculator2/Licenses.java
deleted file mode 100644
index 4af1ea6..0000000
--- a/src/com/android/calculator2/Licenses.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.android.calculator2;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.view.MenuItem;
-import android.webkit.WebView;
-
-public class Licenses extends Activity {
-
- private static final String LICENSE_URL = "file:///android_asset/licenses.html";
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- final WebView webView = new WebView(this);
- webView.loadUrl(LICENSE_URL);
-
- setContentView(webView);
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- if (item.getItemId() == android.R.id.home) {
- onBackPressed();
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-}
diff --git a/src/com/android/calculator2/StringUtils.java b/src/com/android/calculator2/StringUtils.java
deleted file mode 100644
index 4eef0e7..0000000
--- a/src/com/android/calculator2/StringUtils.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.calculator2;
-
-/**
- * Some helpful methods operating on strings.
- */
-
-public class StringUtils {
-
- /**
- * Return a string with n copies of c.
- */
- public static String repeat(char c, int n) {
- final StringBuilder result = new StringBuilder();
- for (int i = 0; i < n; ++i) {
- result.append(c);
- }
- return result.toString();
- }
-
- /**
- * Return a copy of the supplied string with commas added every three digits.
- * The substring indicated by the supplied range is assumed to contain only
- * a whole number, with no decimal point.
- * Inserting a digit separator every 3 digits appears to be
- * at least somewhat acceptable, though not necessarily preferred, everywhere.
- * The grouping separator in the result is NOT localized.
- */
- public static String addCommas(String s, int begin, int end) {
- // Resist the temptation to use Java's NumberFormat, which converts to long or double
- // and hence doesn't handle very large numbers.
- StringBuilder result = new StringBuilder();
- int current = begin;
- while (current < end && (s.charAt(current) == '-' || s.charAt(current) == ' ')) {
- ++current;
- }
- result.append(s, begin, current);
- while (current < end) {
- result.append(s.charAt(current));
- ++current;
- if ((end - current) % 3 == 0 && end != current) {
- result.append(',');
- }
- }
- return result.toString();
- }
-
- /**
- * Ignoring all occurrences of c in both strings, check whether old is a prefix of new.
- * If so, return the remaining subsequence of whole. If not, return null.
- */
- public static CharSequence getExtensionIgnoring(CharSequence whole, CharSequence prefix,
- char c) {
- int wIndex = 0;
- int pIndex = 0;
- final int wLen = whole.length();
- final int pLen = prefix.length();
- while (true) {
- while (pIndex < pLen && prefix.charAt(pIndex) == c) {
- ++pIndex;
- }
- while (wIndex < wLen && whole.charAt(wIndex) == c) {
- ++wIndex;
- }
- if (pIndex == pLen) {
- break;
- }
- if (wIndex == wLen || whole.charAt(wIndex) != prefix.charAt(pIndex) ) {
- return null;
- }
- ++pIndex;
- ++wIndex;
- }
- while (wIndex < wLen && whole.charAt(wIndex) == c) {
- ++wIndex;
- }
- return whole.subSequence(wIndex, wLen);
- }
-}
diff --git a/src/com/android/calculator2/UnifiedReal.java b/src/com/android/calculator2/UnifiedReal.java
deleted file mode 100644
index f85dd3e..0000000
--- a/src/com/android/calculator2/UnifiedReal.java
+++ /dev/null
@@ -1,1285 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.calculator2;
-
-import java.math.BigInteger;
-import com.hp.creals.CR;
-import com.hp.creals.UnaryCRFunction;
-
-/**
- * Computable real numbers, represented so that we can get exact decidable comparisons
- * for a number of interesting special cases, including rational computations.
- *
- * A real number is represented as the product of two numbers with different representations:
- * A) A BoundedRational that can only represent a subset of the rationals, but supports
- * exact computable comparisons.
- * B) A lazily evaluated "constructive real number" that provides operations to evaluate
- * itself to any requested number of digits.
- * Whenever possible, we choose (B) to be one of a small set of known constants about which we
- * know more. For example, whenever we can, we represent rationals such that (B) is 1.
- * This scheme allows us to do some very limited symbolic computation on numbers when both
- * have the same (B) value, as well as in some other situations. We try to maximize that
- * possibility.
- *
- * Arithmetic operations and operations that produce finite approximations may throw unchecked
- * exceptions produced by the underlying CR and BoundedRational packages, including
- * CR.PrecisionOverflowException and CR.AbortedException.
- */
-public class UnifiedReal {
-
- private final BoundedRational mRatFactor;
- private final CR mCrFactor;
- // TODO: It would be helpful to add flags to indicate whether the result is known
- // irrational, etc. This sometimes happens even if mCrFactor is not one of the known ones.
- // And exact comparisons between rationals and known irrationals are decidable.
-
- /**
- * Perform some nontrivial consistency checks.
- * @hide
- */
- public static boolean enableChecks = true;
-
- private static void check(boolean b) {
- if (!b) {
- throw new AssertionError();
- }
- }
-
- private UnifiedReal(BoundedRational rat, CR cr) {
- if (rat == null) {
- throw new ArithmeticException("Building UnifiedReal from null");
- }
- // We don't normally traffic in null CRs, and hence don't test explicitly.
- mCrFactor = cr;
- mRatFactor = rat;
- }
-
- public UnifiedReal(CR cr) {
- this(BoundedRational.ONE, cr);
- }
-
- public UnifiedReal(BoundedRational rat) {
- this(rat, CR_ONE);
- }
-
- public UnifiedReal(BigInteger n) {
- this(new BoundedRational(n));
- }
-
- public UnifiedReal(long n) {
- this(new BoundedRational(n));
- }
-
- public static UnifiedReal valueOf(double x) {
- if (x == 0.0 || x == 1.0) {
- return valueOf((long) x);
- }
- return new UnifiedReal(BoundedRational.valueOf(x));
- }
-
- public static UnifiedReal valueOf(long x) {
- if (x == 0) {
- return UnifiedReal.ZERO;
- } else if (x == 1) {
- return UnifiedReal.ONE;
- } else {
- return new UnifiedReal(BoundedRational.valueOf(x));
- }
- }
-
- // Various helpful constants
- private final static BigInteger BIG_24 = BigInteger.valueOf(24);
- private final static int DEFAULT_COMPARE_TOLERANCE = -1000;
-
- // Well-known CR constants we try to use in the mCrFactor position:
- private final static CR CR_ONE = CR.ONE;
- private final static CR CR_PI = CR.PI;
- private final static CR CR_E = CR.ONE.exp();
- private final static CR CR_SQRT2 = CR.valueOf(2).sqrt();
- private final static CR CR_SQRT3 = CR.valueOf(3).sqrt();
- private final static CR CR_LN2 = CR.valueOf(2).ln();
- private final static CR CR_LN3 = CR.valueOf(3).ln();
- private final static CR CR_LN5 = CR.valueOf(5).ln();
- private final static CR CR_LN6 = CR.valueOf(6).ln();
- private final static CR CR_LN7 = CR.valueOf(7).ln();
- private final static CR CR_LN10 = CR.valueOf(10).ln();
-
- // Square roots that we try to recognize.
- // We currently recognize only a small fixed collection, since the sqrt() function needs to
- // identify numbers of the form <SQRT[i]>*n^2, and we don't otherwise know of a good
- // algorithm for that.
- private final static CR sSqrts[] = {
- null,
- CR.ONE,
- CR_SQRT2,
- CR_SQRT3,
- null,
- CR.valueOf(5).sqrt(),
- CR.valueOf(6).sqrt(),
- CR.valueOf(7).sqrt(),
- null,
- null,
- CR.valueOf(10).sqrt() };
-
- // Natural logs of small integers that we try to recognize.
- private final static CR sLogs[] = {
- null,
- null,
- CR_LN2,
- CR_LN3,
- null,
- CR_LN5,
- CR_LN6,
- CR_LN7,
- null,
- null,
- CR_LN10 };
-
-
- // Some convenient UnifiedReal constants.
- public static final UnifiedReal PI = new UnifiedReal(CR_PI);
- public static final UnifiedReal E = new UnifiedReal(CR_E);
- public static final UnifiedReal ZERO = new UnifiedReal(BoundedRational.ZERO);
- public static final UnifiedReal ONE = new UnifiedReal(BoundedRational.ONE);
- public static final UnifiedReal MINUS_ONE = new UnifiedReal(BoundedRational.MINUS_ONE);
- public static final UnifiedReal TWO = new UnifiedReal(BoundedRational.TWO);
- public static final UnifiedReal MINUS_TWO = new UnifiedReal(BoundedRational.MINUS_TWO);
- public static final UnifiedReal HALF = new UnifiedReal(BoundedRational.HALF);
- public static final UnifiedReal MINUS_HALF = new UnifiedReal(BoundedRational.MINUS_HALF);
- public static final UnifiedReal TEN = new UnifiedReal(BoundedRational.TEN);
- public static final UnifiedReal RADIANS_PER_DEGREE
- = new UnifiedReal(new BoundedRational(1, 180), CR_PI);
- private static final UnifiedReal SIX = new UnifiedReal(6);
- private static final UnifiedReal HALF_SQRT2 = new UnifiedReal(BoundedRational.HALF, CR_SQRT2);
- private static final UnifiedReal SQRT3 = new UnifiedReal(CR_SQRT3);
- private static final UnifiedReal HALF_SQRT3 = new UnifiedReal(BoundedRational.HALF, CR_SQRT3);
- private static final UnifiedReal THIRD_SQRT3 = new UnifiedReal(BoundedRational.THIRD, CR_SQRT3);
- private static final UnifiedReal PI_OVER_2 = new UnifiedReal(BoundedRational.HALF, CR_PI);
- private static final UnifiedReal PI_OVER_3 = new UnifiedReal(BoundedRational.THIRD, CR_PI);
- private static final UnifiedReal PI_OVER_4 = new UnifiedReal(BoundedRational.QUARTER, CR_PI);
- private static final UnifiedReal PI_OVER_6 = new UnifiedReal(BoundedRational.SIXTH, CR_PI);
-
-
- /**
- * Given a constructive real cr, try to determine whether cr is the square root of
- * a small integer. If so, return its square as a BoundedRational. Otherwise return null.
- * We make this determination by simple table lookup, so spurious null returns are
- * entirely possible, or even likely.
- */
- private static BoundedRational getSquare(CR cr) {
- for (int i = 0; i < sSqrts.length; ++i) {
- if (sSqrts[i] == cr) {
- return new BoundedRational(i);
- }
- }
- return null;
- }
-
- /**
- * Given a constructive real cr, try to determine whether cr is the logarithm of a small
- * integer. If so, return exp(cr) as a BoundedRational. Otherwise return null.
- * We make this determination by simple table lookup, so spurious null returns are
- * entirely possible, or even likely.
- */
- private BoundedRational getExp(CR cr) {
- for (int i = 0; i < sLogs.length; ++i) {
- if (sLogs[i] == cr) {
- return new BoundedRational(i);
- }
- }
- return null;
- }
-
- /**
- * If the argument is a well-known constructive real, return its name.
- * The name of "CR_ONE" is the empty string.
- * No named constructive reals are rational multiples of each other.
- * Thus two UnifiedReals with different named mCrFactors can be equal only if both
- * mRatFactors are zero or possibly if one is CR_PI and the other is CR_E.
- * (The latter is apparently an open problem.)
- */
- private static String crName(CR cr) {
- if (cr == CR_ONE) {
- return "";
- }
- if (cr == CR_PI) {
- return "\u03C0"; // GREEK SMALL LETTER PI
- }
- if (cr == CR_E) {
- return "e";
- }
- for (int i = 0; i < sSqrts.length; ++i) {
- if (cr == sSqrts[i]) {
- return "\u221A" /* SQUARE ROOT */ + i;
- }
- }
- for (int i = 0; i < sLogs.length; ++i) {
- if (cr == sLogs[i]) {
- return "ln(" + i + ")";
- }
- }
- return null;
- }
-
- /**
- * Would crName() return non-Null?
- */
- private static boolean isNamed(CR cr) {
- if (cr == CR_ONE || cr == CR_PI || cr == CR_E) {
- return true;
- }
- for (CR r: sSqrts) {
- if (cr == r) {
- return true;
- }
- }
- for (CR r: sLogs) {
- if (cr == r) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Is cr known to be algebraic (as opposed to transcendental)?
- * Currently only produces meaningful results for the above known special
- * constructive reals.
- */
- private static boolean definitelyAlgebraic(CR cr) {
- return cr == CR_ONE || getSquare(cr) != null;
- }
-
- /**
- * Is this number known to be rational?
- */
- public boolean definitelyRational() {
- return mCrFactor == CR_ONE || mRatFactor.signum() == 0;
- }
-
- /**
- * Is this number known to be irrational?
- * TODO: We could track the fact that something is irrational with an explicit flag, which
- * could cover many more cases. Whether that matters in practice is TBD.
- */
- public boolean definitelyIrrational() {
- return !definitelyRational() && isNamed(mCrFactor);
- }
-
- /**
- * Is this number known to be algebraic?
- */
- public boolean definitelyAlgebraic() {
- return definitelyAlgebraic(mCrFactor) || mRatFactor.signum() == 0;
- }
-
- /**
- * Is this number known to be transcendental?
- */
- public boolean definitelyTranscendental() {
- return !definitelyAlgebraic() && isNamed(mCrFactor);
- }
-
-
- /**
- * Is it known that the two constructive reals differ by something other than a
- * a rational factor, i.e. is it known that two UnifiedReals
- * with those mCrFactors will compare unequal unless both mRatFactors are zero?
- * If this returns true, then a comparison of two UnifiedReals using those two
- * mCrFactors cannot diverge, though we don't know of a good runtime bound.
- */
- private static boolean definitelyIndependent(CR r1, CR r2) {
- // The question here is whether r1 = x*r2, where x is rational, where r1 and r2
- // are in our set of special known CRs, can have a solution.
- // This cannot happen if one is CR_ONE and the other is not.
- // (Since all others are irrational.)
- // This cannot happen for two named square roots, which have no repeated factors.
- // (To see this, square both sides of the equation and factor. Each prime
- // factor in the numerator and denominator occurs twice.)
- // This cannot happen for e or pi on one side, and a square root on the other.
- // (One is transcendental, the other is algebraic.)
- // This cannot happen for two of our special natural logs.
- // (Otherwise ln(m) = (a/b)ln(n) ==> m = n^(a/b) ==> m^b = n^a, which is impossible
- // because either m or n includes a prime factor not shared by the other.)
- // This cannot happen for a log and a square root.
- // (The Lindemann-Weierstrass theorem tells us, among other things, that if
- // a is algebraic, then exp(a) is transcendental. Thus if l in our finite
- // set of logs where algebraic, expl(l), must be transacendental.
- // But exp(l) is an integer. Thus the logs are transcendental. But of course the
- // square roots are algebraic. Thus they can't be rational multiples.)
- // Unfortunately, we do not know whether e/pi is rational.
- if (r1 == r2) {
- return false;
- }
- CR other;
- if (r1 == CR_E || r1 == CR_PI) {
- return definitelyAlgebraic(r2);
- }
- if (r2 == CR_E || r2 == CR_PI) {
- return definitelyAlgebraic(r1);
- }
- return isNamed(r1) && isNamed(r2);
- }
-
- /**
- * Convert to String reflecting raw representation.
- * Debug or log messages only, not pretty.
- */
- public String toString() {
- return mRatFactor.toString() + "*" + mCrFactor.toString();
- }
-
- /**
- * Convert to readable String.
- * Intended for user output. Produces exact expression when possible.
- */
- public String toNiceString() {
- if (mCrFactor == CR_ONE || mRatFactor.signum() == 0) {
- return mRatFactor.toNiceString();
- }
- String name = crName(mCrFactor);
- if (name != null) {
- BigInteger bi = BoundedRational.asBigInteger(mRatFactor);
- if (bi != null) {
- if (bi.equals(BigInteger.ONE)) {
- return name;
- }
- return mRatFactor.toNiceString() + name;
- }
- return "(" + mRatFactor.toNiceString() + ")" + name;
- }
- if (mRatFactor.equals(BoundedRational.ONE)) {
- return mCrFactor.toString();
- }
- return crValue().toString();
- }
-
- /**
- * Will toNiceString() produce an exact representation?
- */
- public boolean exactlyDisplayable() {
- return crName(mCrFactor) != null;
- }
-
- // Number of extra bits used in evaluation below to prefer truncation to rounding.
- // Must be <= 30.
- private final static int EXTRA_PREC = 10;
-
- /*
- * Returns a truncated representation of the result.
- * If exactlyTruncatable(), we round correctly towards zero. Otherwise the resulting digit
- * string may occasionally be rounded up instead.
- * Always includes a decimal point in the result.
- * The result includes n digits to the right of the decimal point.
- * @param n result precision, >= 0
- */
- public String toStringTruncated(int n) {
- if (mCrFactor == CR_ONE || mRatFactor == BoundedRational.ZERO) {
- return mRatFactor.toStringTruncated(n);
- }
- final CR scaled = CR.valueOf(BigInteger.TEN.pow(n)).multiply(crValue());
- boolean negative = false;
- BigInteger intScaled;
- if (exactlyTruncatable()) {
- intScaled = scaled.get_appr(0);
- if (intScaled.signum() < 0) {
- negative = true;
- intScaled = intScaled.negate();
- }
- if (CR.valueOf(intScaled).compareTo(scaled.abs()) > 0) {
- intScaled = intScaled.subtract(BigInteger.ONE);
- }
- check(CR.valueOf(intScaled).compareTo(scaled.abs()) < 0);
- } else {
- // Approximate case. Exact comparisons are impossible.
- intScaled = scaled.get_appr(-EXTRA_PREC);
- if (intScaled.signum() < 0) {
- negative = true;
- intScaled = intScaled.negate();
- }
- intScaled = intScaled.shiftRight(EXTRA_PREC);
- }
- String digits = intScaled.toString();
- int len = digits.length();
- if (len < n + 1) {
- digits = StringUtils.repeat('0', n + 1 - len) + digits;
- len = n + 1;
- }
- return (negative ? "-" : "") + digits.substring(0, len - n) + "."
- + digits.substring(len - n);
- }
-
- /*
- * Can we compute correctly truncated approximations of this number?
- */
- public boolean exactlyTruncatable() {
- // If the value is known rational, we can do exact comparisons.
- // If the value is known irrational, then we can safely compare to rational approximations;
- // equality is impossible; hence the comparison must converge.
- // The only problem cases are the ones in which we don't know.
- return mCrFactor == CR_ONE || mRatFactor == BoundedRational.ZERO || definitelyIrrational();
- }
-
- /**
- * Return a double approximation.
- * Rational arguments are currently rounded to nearest, with ties away from zero.
- * TODO: Improve rounding.
- */
- public double doubleValue() {
- if (mCrFactor == CR_ONE) {
- return mRatFactor.doubleValue(); // Hopefully correctly rounded
- } else {
- return crValue().doubleValue(); // Approximately correctly rounded
- }
- }
-
- public CR crValue() {
- return mRatFactor.crValue().multiply(mCrFactor);
- }
-
- /**
- * Are this and r exactly comparable?
- */
- public boolean isComparable(UnifiedReal u) {
- // We check for ONE only to speed up the common case.
- // The use of a tolerance here means we can spuriously return false, not true.
- return mCrFactor == u.mCrFactor
- && (isNamed(mCrFactor) || mCrFactor.signum(DEFAULT_COMPARE_TOLERANCE) != 0)
- || mRatFactor.signum() == 0 && u.mRatFactor.signum() == 0
- || definitelyIndependent(mCrFactor, u.mCrFactor)
- || crValue().compareTo(u.crValue(), DEFAULT_COMPARE_TOLERANCE) != 0;
- }
-
- /**
- * Return +1 if this is greater than r, -1 if this is less than r, or 0 of the two are
- * known to be equal.
- * May diverge if the two are equal and !isComparable(r).
- */
- public int compareTo(UnifiedReal u) {
- if (definitelyZero() && u.definitelyZero()) return 0;
- if (mCrFactor == u.mCrFactor) {
- int signum = mCrFactor.signum(); // Can diverge if mCRFactor == 0.
- return signum * mRatFactor.compareTo(u.mRatFactor);
- }
- return crValue().compareTo(u.crValue()); // Can also diverge.
- }
-
- /**
- * Return +1 if this is greater than r, -1 if this is less than r, or possibly 0 of the two are
- * within 2^a of each other.
- */
- public int compareTo(UnifiedReal u, int a) {
- if (isComparable(u)) {
- return compareTo(u);
- } else {
- return crValue().compareTo(u.crValue(), a);
- }
- }
-
- /**
- * Return compareTo(ZERO, a).
- */
- public int signum(int a) {
- return compareTo(ZERO, a);
- }
-
- /**
- * Return compareTo(ZERO).
- * May diverge for ZERO argument if !isComparable(ZERO).
- */
- public int signum() {
- return compareTo(ZERO);
- }
-
- /**
- * Equality comparison. May erroneously return true if values differ by less than 2^a,
- * and !isComparable(u).
- */
- public boolean approxEquals(UnifiedReal u, int a) {
- if (isComparable(u)) {
- if (definitelyIndependent(mCrFactor, u.mCrFactor)
- && (mRatFactor.signum() != 0 || u.mRatFactor.signum() != 0)) {
- // No need to actually evaluate, though we don't know which is larger.
- return false;
- } else {
- return compareTo(u) == 0;
- }
- }
- return crValue().compareTo(u.crValue(), a) == 0;
- }
-
- /**
- * Returns true if values are definitely known to be equal, false in all other cases.
- * This does not satisfy the contract for Object.equals().
- */
- public boolean definitelyEquals(UnifiedReal u) {
- return isComparable(u) && compareTo(u) == 0;
- }
-
- @Override
- public int hashCode() {
- // Better useless than wrong. Probably.
- return 0;
- }
-
- @Override
- public boolean equals(Object r) {
- if (r == null || !(r instanceof UnifiedReal)) {
- return false;
- }
- // This is almost certainly a programming error. Don't even try.
- throw new AssertionError("Can't compare UnifiedReals for exact equality");
- }
-
- /**
- * Returns true if values are definitely known not to be equal, false in all other cases.
- * Performs no approximate evaluation.
- */
- public boolean definitelyNotEquals(UnifiedReal u) {
- boolean isNamed = isNamed(mCrFactor);
- boolean uIsNamed = isNamed(u.mCrFactor);
- if (isNamed && uIsNamed) {
- if (definitelyIndependent(mCrFactor, u.mCrFactor)) {
- return mRatFactor.signum() != 0 || u.mRatFactor.signum() != 0;
- } else if (mCrFactor == u.mCrFactor) {
- return !mRatFactor.equals(u.mRatFactor);
- }
- return !mRatFactor.equals(u.mRatFactor);
- }
- if (mRatFactor.signum() == 0) {
- return uIsNamed && u.mRatFactor.signum() != 0;
- }
- if (u.mRatFactor.signum() == 0) {
- return isNamed && mRatFactor.signum() != 0;
- }
- return false;
- }
-
- // And some slightly faster convenience functions for special cases:
-
- public boolean definitelyZero() {
- return mRatFactor.signum() == 0;
- }
-
- /**
- * Can this number be determined to be definitely nonzero without performing approximate
- * evaluation?
- */
- public boolean definitelyNonZero() {
- return isNamed(mCrFactor) && mRatFactor.signum() != 0;
- }
-
- public boolean definitelyOne() {
- return mCrFactor == CR_ONE && mRatFactor.equals(BoundedRational.ONE);
- }
-
- /**
- * Return equivalent BoundedRational, if known to exist, null otherwise
- */
- public BoundedRational boundedRationalValue() {
- if (mCrFactor == CR_ONE || mRatFactor.signum() == 0) {
- return mRatFactor;
- }
- return null;
- }
-
- /**
- * Returns equivalent BigInteger result if it exists, null if not.
- */
- public BigInteger bigIntegerValue() {
- final BoundedRational r = boundedRationalValue();
- return BoundedRational.asBigInteger(r);
- }
-
- public UnifiedReal add(UnifiedReal u) {
- if (mCrFactor == u.mCrFactor) {
- BoundedRational nRatFactor = BoundedRational.add(mRatFactor, u.mRatFactor);
- if (nRatFactor != null) {
- return new UnifiedReal(nRatFactor, mCrFactor);
- }
- }
- if (definitelyZero()) {
- // Avoid creating new mCrFactor, even if they don't currently match.
- return u;
- }
- if (u.definitelyZero()) {
- return this;
- }
- return new UnifiedReal(crValue().add(u.crValue()));
- }
-
- public UnifiedReal negate() {
- return new UnifiedReal(BoundedRational.negate(mRatFactor), mCrFactor);
- }
-
- public UnifiedReal subtract(UnifiedReal u) {
- return add(u.negate());
- }
-
- public UnifiedReal multiply(UnifiedReal u) {
- // Preserve a preexisting mCrFactor when we can.
- if (mCrFactor == CR_ONE) {
- BoundedRational nRatFactor = BoundedRational.multiply(mRatFactor, u.mRatFactor);
- if (nRatFactor != null) {
- return new UnifiedReal(nRatFactor, u.mCrFactor);
- }
- }
- if (u.mCrFactor == CR_ONE) {
- BoundedRational nRatFactor = BoundedRational.multiply(mRatFactor, u.mRatFactor);
- if (nRatFactor != null) {
- return new UnifiedReal(nRatFactor, mCrFactor);
- }
- }
- if (definitelyZero() || u.definitelyZero()) {
- return ZERO;
- }
- if (mCrFactor == u.mCrFactor) {
- BoundedRational square = getSquare(mCrFactor);
- if (square != null) {
- BoundedRational nRatFactor = BoundedRational.multiply(
- BoundedRational.multiply(square, mRatFactor), u.mRatFactor);
- if (nRatFactor != null) {
- return new UnifiedReal(nRatFactor);
- }
- }
- }
- // Probably a bit cheaper to multiply component-wise.
- BoundedRational nRatFactor = BoundedRational.multiply(mRatFactor, u.mRatFactor);
- if (nRatFactor != null) {
- return new UnifiedReal(nRatFactor, mCrFactor.multiply(u.mCrFactor));
- }
- return new UnifiedReal(crValue().multiply(u.crValue()));
- }
-
- public static class ZeroDivisionException extends ArithmeticException {
- public ZeroDivisionException() {
- super("Division by zero");
- }
- }
-
- /**
- * Return the reciprocal.
- */
- public UnifiedReal inverse() {
- if (definitelyZero()) {
- throw new ZeroDivisionException();
- }
- BoundedRational square = getSquare(mCrFactor);
- if (square != null) {
- // 1/sqrt(n) = sqrt(n)/n
- BoundedRational nRatFactor = BoundedRational.inverse(
- BoundedRational.multiply(mRatFactor, square));
- if (nRatFactor != null) {
- return new UnifiedReal(nRatFactor, mCrFactor);
- }
- }
- return new UnifiedReal(BoundedRational.inverse(mRatFactor), mCrFactor.inverse());
- }
-
- public UnifiedReal divide(UnifiedReal u) {
- if (mCrFactor == u.mCrFactor) {
- if (u.definitelyZero()) {
- throw new ZeroDivisionException();
- }
- BoundedRational nRatFactor = BoundedRational.divide(mRatFactor, u.mRatFactor);
- if (nRatFactor != null) {
- return new UnifiedReal(nRatFactor, CR_ONE);
- }
- }
- return multiply(u.inverse());
- }
-
- /**
- * Return the square root.
- * This may fail to return a known rational value, even when the result is rational.
- */
- public UnifiedReal sqrt() {
- if (definitelyZero()) {
- return ZERO;
- }
- if (mCrFactor == CR_ONE) {
- BoundedRational ratSqrt;
- // Check for all arguments of the form <perfect rational square> * small_int,
- // where small_int has a known sqrt. This includes the small_int = 1 case.
- for (int divisor = 1; divisor < sSqrts.length; ++divisor) {
- if (sSqrts[divisor] != null) {
- ratSqrt = BoundedRational.sqrt(
- BoundedRational.divide(mRatFactor, new BoundedRational(divisor)));
- if (ratSqrt != null) {
- return new UnifiedReal(ratSqrt, sSqrts[divisor]);
- }
- }
- }
- }
- return new UnifiedReal(crValue().sqrt());
- }
-
- /**
- * Return (this mod 2pi)/(pi/6) as a BigInteger, or null if that isn't easily possible.
- */
- private BigInteger getPiTwelfths() {
- if (definitelyZero()) return BigInteger.ZERO;
- if (mCrFactor == CR_PI) {
- BigInteger quotient = BoundedRational.asBigInteger(
- BoundedRational.multiply(mRatFactor, BoundedRational.TWELVE));
- if (quotient == null) {
- return null;
- }
- return quotient.mod(BIG_24);
- }
- return null;
- }
-
- /**
- * Computer the sin() for an integer multiple n of pi/12, if easily representable.
- * @param n value between 0 and 23 inclusive.
- */
- private static UnifiedReal sinPiTwelfths(int n) {
- if (n >= 12) {
- UnifiedReal negResult = sinPiTwelfths(n - 12);
- return negResult == null ? null : negResult.negate();
- }
- switch (n) {
- case 0:
- return ZERO;
- case 2: // 30 degrees
- return HALF;
- case 3: // 45 degrees
- return HALF_SQRT2;
- case 4: // 60 degrees
- return HALF_SQRT3;
- case 6:
- return ONE;
- case 8:
- return HALF_SQRT3;
- case 9:
- return HALF_SQRT2;
- case 10:
- return HALF;
- default:
- return null;
- }
- }
-
- public UnifiedReal sin() {
- BigInteger piTwelfths = getPiTwelfths();
- if (piTwelfths != null) {
- UnifiedReal result = sinPiTwelfths(piTwelfths.intValue());
- if (result != null) {
- return result;
- }
- }
- return new UnifiedReal(crValue().sin());
- }
-
- private static UnifiedReal cosPiTwelfths(int n) {
- int sinArg = n + 6;
- if (sinArg >= 24) {
- sinArg -= 24;
- }
- return sinPiTwelfths(sinArg);
- }
-
- public UnifiedReal cos() {
- BigInteger piTwelfths = getPiTwelfths();
- if (piTwelfths != null) {
- UnifiedReal result = cosPiTwelfths(piTwelfths.intValue());
- if (result != null) {
- return result;
- }
- }
- return new UnifiedReal(crValue().cos());
- }
-
- public UnifiedReal tan() {
- BigInteger piTwelfths = getPiTwelfths();
- if (piTwelfths != null) {
- int i = piTwelfths.intValue();
- if (i == 6 || i == 18) {
- throw new ArithmeticException("Tangent undefined");
- }
- UnifiedReal top = sinPiTwelfths(i);
- UnifiedReal bottom = cosPiTwelfths(i);
- if (top != null && bottom != null) {
- return top.divide(bottom);
- }
- }
- return sin().divide(cos());
- }
-
- // Throw an exception if the argument is definitely out of bounds for asin or acos.
- private void checkAsinDomain() {
- if (isComparable(ONE) && (compareTo(ONE) > 0 || compareTo(MINUS_ONE) < 0)) {
- throw new ArithmeticException("inverse trig argument out of range");
- }
- }
-
- /**
- * Return asin(n/2). n is between -2 and 2.
- */
- public static UnifiedReal asinHalves(int n){
- if (n < 0) {
- return (asinHalves(-n).negate());
- }
- switch (n) {
- case 0:
- return ZERO;
- case 1:
- return new UnifiedReal(BoundedRational.SIXTH, CR.PI);
- case 2:
- return new UnifiedReal(BoundedRational.HALF, CR.PI);
- }
- throw new AssertionError("asinHalves: Bad argument");
- }
-
- /**
- * Return asin of this, assuming this is not an integral multiple of a half.
- */
- public UnifiedReal asinNonHalves()
- {
- if (compareTo(ZERO, -10) < 0) {
- return negate().asinNonHalves().negate();
- }
- if (definitelyEquals(HALF_SQRT2)) {
- return new UnifiedReal(BoundedRational.QUARTER, CR_PI);
- }
- if (definitelyEquals(HALF_SQRT3)) {
- return new UnifiedReal(BoundedRational.THIRD, CR_PI);
- }
- return new UnifiedReal(crValue().asin());
- }
-
- public UnifiedReal asin() {
- checkAsinDomain();
- final BigInteger halves = multiply(TWO).bigIntegerValue();
- if (halves != null) {
- return asinHalves(halves.intValue());
- }
- if (mCrFactor == CR.ONE || mCrFactor != CR_SQRT2 ||mCrFactor != CR_SQRT3) {
- return asinNonHalves();
- }
- return new UnifiedReal(crValue().asin());
- }
-
- public UnifiedReal acos() {
- return PI_OVER_2.subtract(asin());
- }
-
- public UnifiedReal atan() {
- if (compareTo(ZERO, -10) < 0) {
- return negate().atan().negate();
- }
- final BigInteger asBI = bigIntegerValue();
- if (asBI != null && asBI.compareTo(BigInteger.ONE) <= 0) {
- final int asInt = asBI.intValue();
- // These seem to be all rational cases:
- switch (asInt) {
- case 0:
- return ZERO;
- case 1:
- return PI_OVER_4;
- default:
- throw new AssertionError("Impossible r_int");
- }
- }
- if (definitelyEquals(THIRD_SQRT3)) {
- return PI_OVER_6;
- }
- if (definitelyEquals(SQRT3)) {
- return PI_OVER_3;
- }
- return new UnifiedReal(UnaryCRFunction.atanFunction.execute(crValue()));
- }
-
- private static final BigInteger BIG_TWO = BigInteger.valueOf(2);
-
- // The (in abs value) integral exponent for which we attempt to use a recursive
- // algorithm for evaluating pow(). The recursive algorithm works independent of the sign of the
- // base, and can produce rational results. But it can become slow for very large exponents.
- private static final BigInteger RECURSIVE_POW_LIMIT = BigInteger.valueOf(1000);
- // The corresponding limit when we're using rational arithmetic. This should fail fast
- // anyway, but we avoid ridiculously deep recursion.
- private static final BigInteger HARD_RECURSIVE_POW_LIMIT = BigInteger.ONE.shiftLeft(1000);
-
- /**
- * Compute an integral power of a constructive real, using the standard recursive algorithm.
- * exp is known to be positive.
- */
- private static CR recursivePow(CR base, BigInteger exp) {
- if (exp.equals(BigInteger.ONE)) {
- return base;
- }
- if (exp.testBit(0)) {
- return base.multiply(recursivePow(base, exp.subtract(BigInteger.ONE)));
- }
- CR tmp = recursivePow(base, exp.shiftRight(1));
- if (Thread.interrupted()) {
- throw new CR.AbortedException();
- }
- return tmp.multiply(tmp);
- }
-
- /**
- * Compute an integral power of a constructive real, using the exp function when
- * we safely can. Use recursivePow when we can't. exp is known to be nozero.
- */
- private UnifiedReal expLnPow(BigInteger exp) {
- int sign = signum(DEFAULT_COMPARE_TOLERANCE);
- if (sign > 0) {
- // Safe to take the log. This avoids deep recursion for huge exponents, which
- // may actually make sense here.
- return new UnifiedReal(crValue().ln().multiply(CR.valueOf(exp)).exp());
- } else if (sign < 0) {
- CR result = crValue().negate().ln().multiply(CR.valueOf(exp)).exp();
- if (exp.testBit(0) /* odd exponent */) {
- result = result.negate();
- }
- return new UnifiedReal(result);
- } else {
- // Base of unknown sign with integer exponent. Use a recursive computation.
- // (Another possible option would be to use the absolute value of the base, and then
- // adjust the sign at the end. But that would have to be done in the CR
- // implementation.)
- if (exp.signum() < 0) {
- // This may be very expensive if exp.negate() is large.
- return new UnifiedReal(recursivePow(crValue(), exp.negate()).inverse());
- } else {
- return new UnifiedReal(recursivePow(crValue(), exp));
- }
- }
- }
-
-
- /**
- * Compute an integral power of this.
- * This recurses roughly as deeply as the number of bits in the exponent, and can, in
- * ridiculous cases, result in a stack overflow.
- */
- private UnifiedReal pow(BigInteger exp) {
- if (exp.equals(BigInteger.ONE)) {
- return this;
- }
- if (exp.signum() == 0) {
- // Questionable if base has undefined value or is 0.
- // Java.lang.Math.pow() returns 1 anyway, so we do the same.
- return ONE;
- }
- BigInteger absExp = exp.abs();
- if (mCrFactor == CR_ONE && absExp.compareTo(HARD_RECURSIVE_POW_LIMIT) <= 0) {
- final BoundedRational ratPow = mRatFactor.pow(exp);
- // We count on this to fail, e.g. for very large exponents, when it would
- // otherwise be too expensive.
- if (ratPow != null) {
- return new UnifiedReal(ratPow);
- }
- }
- if (absExp.compareTo(RECURSIVE_POW_LIMIT) > 0) {
- return expLnPow(exp);
- }
- BoundedRational square = getSquare(mCrFactor);
- if (square != null) {
- final BoundedRational nRatFactor =
- BoundedRational.multiply(mRatFactor.pow(exp), square.pow(exp.shiftRight(1)));
- if (nRatFactor != null) {
- if (exp.and(BigInteger.ONE).intValue() == 1) {
- // Odd power: Multiply by remaining square root.
- return new UnifiedReal(nRatFactor, mCrFactor);
- } else {
- return new UnifiedReal(nRatFactor);
- }
- }
- }
- return expLnPow(exp);
- }
-
- /**
- * Return this ^ expon.
- * This is really only well-defined for a positive base, particularly since
- * 0^x is not continuous at zero. (0^0 = 1 (as is epsilon^0), but 0^epsilon is 0.
- * We nonetheless try to do reasonable things at zero, when we recognize that case.
- */
- public UnifiedReal pow(UnifiedReal expon) {
- if (mCrFactor == CR_E) {
- if (mRatFactor.equals(BoundedRational.ONE)) {
- return expon.exp();
- } else {
- UnifiedReal ratPart = new UnifiedReal(mRatFactor).pow(expon);
- return expon.exp().multiply(ratPart);
- }
- }
- final BoundedRational expAsBR = expon.boundedRationalValue();
- if (expAsBR != null) {
- BigInteger expAsBI = BoundedRational.asBigInteger(expAsBR);
- if (expAsBI != null) {
- return pow(expAsBI);
- } else {
- // Check for exponent that is a multiple of a half.
- expAsBI = BoundedRational.asBigInteger(
- BoundedRational.multiply(BoundedRational.TWO, expAsBR));
- if (expAsBI != null) {
- return pow(expAsBI).sqrt();
- }
- }
- }
- // If the exponent were known zero, we would have handled it above.
- if (definitelyZero()) {
- return ZERO;
- }
- int sign = signum(DEFAULT_COMPARE_TOLERANCE);
- if (sign < 0) {
- throw new ArithmeticException("Negative base for pow() with non-integer exponent");
- }
- return new UnifiedReal(crValue().ln().multiply(expon.crValue()).exp());
- }
-
- /**
- * Raise the argument to the 16th power.
- */
- private static long pow16(int n) {
- if (n > 10) {
- throw new AssertionError("Unexpected pow16 argument");
- }
- long result = n*n;
- result *= result;
- result *= result;
- result *= result;
- return result;
- }
-
- /**
- * Return the integral log with respect to the given base if it exists, 0 otherwise.
- * n is presumed positive.
- */
- private static long getIntLog(BigInteger n, int base) {
- double nAsDouble = n.doubleValue();
- double approx = Math.log(nAsDouble)/Math.log(base);
- // A relatively quick test first.
- // Unfortunately, this doesn't help for values to big to fit in a Double.
- if (!Double.isInfinite(nAsDouble) && Math.abs(approx - Math.rint(approx)) > 1.0e-6) {
- return 0;
- }
- long result = 0;
- BigInteger remaining = n;
- BigInteger bigBase = BigInteger.valueOf(base);
- BigInteger base16th = null; // base^16, computed lazily
- while (n.mod(bigBase).signum() == 0) {
- if (Thread.interrupted()) {
- throw new CR.AbortedException();
- }
- n = n.divide(bigBase);
- ++result;
- // And try a slightly faster computation for large n:
- if (base16th == null) {
- base16th = BigInteger.valueOf(pow16(base));
- }
- while (n.mod(base16th).signum() == 0) {
- n = n.divide(base16th);
- result += 16;
- }
- }
- if (n.equals(BigInteger.ONE)) {
- return result;
- }
- return 0;
- }
-
- public UnifiedReal ln() {
- if (mCrFactor == CR_E) {
- return new UnifiedReal(mRatFactor, CR_ONE).ln().add(ONE);
- }
- if (isComparable(ZERO)) {
- if (signum() <= 0) {
- throw new ArithmeticException("log(non-positive)");
- }
- int compare1 = compareTo(ONE, DEFAULT_COMPARE_TOLERANCE);
- if (compare1 == 0) {
- if (definitelyEquals(ONE)) {
- return ZERO;
- }
- } else if (compare1 < 0) {
- return inverse().ln().negate();
- }
- final BigInteger bi = BoundedRational.asBigInteger(mRatFactor);
- if (bi != null) {
- if (mCrFactor == CR_ONE) {
- // Check for a power of a small integer. We can use sLogs[] to return
- // a more useful answer for those.
- for (int i = 0; i < sLogs.length; ++i) {
- if (sLogs[i] != null) {
- long intLog = getIntLog(bi, i);
- if (intLog != 0) {
- return new UnifiedReal(new BoundedRational(intLog), sLogs[i]);
- }
- }
- }
- } else {
- // Check for n^k * sqrt(n), for which we can also return a more useful answer.
- BoundedRational square = getSquare(mCrFactor);
- if (square != null) {
- int intSquare = square.intValue();
- if (sLogs[intSquare] != null) {
- long intLog = getIntLog(bi, intSquare);
- if (intLog != 0) {
- BoundedRational nRatFactor =
- BoundedRational.add(new BoundedRational(intLog),
- BoundedRational.HALF);
- if (nRatFactor != null) {
- return new UnifiedReal(nRatFactor, sLogs[intSquare]);
- }
- }
- }
- }
- }
- }
- }
- return new UnifiedReal(crValue().ln());
- }
-
- public UnifiedReal exp() {
- if (definitelyEquals(ZERO)) {
- return ONE;
- }
- if (definitelyEquals(ONE)) {
- // Avoid redundant computations, and ensure we recognize all instances as equal.
- return E;
- }
- final BoundedRational crExp = getExp(mCrFactor);
- if (crExp != null) {
- boolean needSqrt = false;
- BoundedRational ratExponent = mRatFactor;
- BigInteger asBI = BoundedRational.asBigInteger(ratExponent);
- if (asBI == null) {
- // check for multiple of one half.
- needSqrt = true;
- ratExponent = BoundedRational.multiply(ratExponent, BoundedRational.TWO);
- }
- BoundedRational nRatFactor = BoundedRational.pow(crExp, ratExponent);
- if (nRatFactor != null) {
- UnifiedReal result = new UnifiedReal(nRatFactor);
- if (needSqrt) {
- result = result.sqrt();
- }
- return result;
- }
- }
- return new UnifiedReal(crValue().exp());
- }
-
-
- /**
- * Generalized factorial.
- * Compute n * (n - step) * (n - 2 * step) * etc. This can be used to compute factorial a bit
- * faster, especially if BigInteger uses sub-quadratic multiplication.
- */
- private static BigInteger genFactorial(long n, long step) {
- if (n > 4 * step) {
- BigInteger prod1 = genFactorial(n, 2 * step);
- if (Thread.interrupted()) {
- throw new CR.AbortedException();
- }
- BigInteger prod2 = genFactorial(n - step, 2 * step);
- if (Thread.interrupted()) {
- throw new CR.AbortedException();
- }
- return prod1.multiply(prod2);
- } else {
- if (n == 0) {
- return BigInteger.ONE;
- }
- BigInteger res = BigInteger.valueOf(n);
- for (long i = n - step; i > 1; i -= step) {
- res = res.multiply(BigInteger.valueOf(i));
- }
- return res;
- }
- }
-
-
- /**
- * Factorial function.
- * Fails if argument is clearly not an integer.
- * May round to nearest integer if value is close.
- */
- public UnifiedReal fact() {
- BigInteger asBI = bigIntegerValue();
- if (asBI == null) {
- asBI = crValue().get_appr(0); // Correct if it was an integer.
- if (!approxEquals(new UnifiedReal(asBI), DEFAULT_COMPARE_TOLERANCE)) {
- throw new ArithmeticException("Non-integral factorial argument");
- }
- }
- if (asBI.signum() < 0) {
- throw new ArithmeticException("Negative factorial argument");
- }
- if (asBI.bitLength() > 20) {
- // Will fail. LongValue() may not work. Punt now.
- throw new ArithmeticException("Factorial argument too big");
- }
- BigInteger biResult = genFactorial(asBI.longValue(), 1);
- BoundedRational nRatFactor = new BoundedRational(biResult);
- return new UnifiedReal(nRatFactor);
- }
-
- /**
- * Return the number of decimal digits to the right of the decimal point required to represent
- * the argument exactly.
- * Return Integer.MAX_VALUE if that's not possible. Never returns a value less than zero, even
- * if r is a power of ten.
- */
- public int digitsRequired() {
- if (mCrFactor == CR_ONE || mRatFactor.signum() == 0) {
- return BoundedRational.digitsRequired(mRatFactor);
- } else {
- return Integer.MAX_VALUE;
- }
- }
-
- /**
- * Return an upper bound on the number of leading zero bits.
- * These are the number of 0 bits
- * to the right of the binary point and to the left of the most significant digit.
- * Return Integer.MAX_VALUE if we cannot bound it.
- */
- public int leadingBinaryZeroes() {
- if (isNamed(mCrFactor)) {
- // Only ln(2) is smaller than one, and could possibly add one zero bit.
- // Adding 3 gives us a somewhat sloppy upper bound.
- final int wholeBits = mRatFactor.wholeNumberBits();
- if (wholeBits == Integer.MIN_VALUE) {
- return Integer.MAX_VALUE;
- }
- if (wholeBits >= 3) {
- return 0;
- } else {
- return -wholeBits + 3;
- }
- }
- return Integer.MAX_VALUE;
- }
-
- /**
- * Is the number of bits to the left of the decimal point greater than bound?
- * The result is inexact: We roughly approximate the whole number bits.
- */
- public boolean approxWholeNumberBitsGreaterThan(int bound) {
- if (isNamed(mCrFactor)) {
- return mRatFactor.wholeNumberBits() > bound;
- } else {
- return crValue().get_appr(bound - 2).bitLength() > 2;
- }
- }
-}