diff options
Diffstat (limited to 'src/com/android/calculator2')
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; - } - } -} |