/* * 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 android.support.annotation.NonNull; import android.support.annotation.StringRes; import android.support.v4.content.ContextCompat; import android.support.v4.view.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() 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 TEXT_COLOR = new Property(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() { if (mCurrentState == CalculatorState.ANIMATE) { throw new AssertionError("impossible state"); } // 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) { throw new AssertionError("onUserInteraction should have ended animation"); } 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() { final FragmentManager manager = getFragmentManager(); if (manager == null || manager.isDestroyed()) { return; } if (getHistoryFragment() != null || !prepareForHistory()) { 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(); } }