Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3937b65f authored by Chenjie Yu's avatar Chenjie Yu
Browse files

Make Calculator compatible with API 21+

For copying from result view and pasting into text view,
we should use context menu for pre-M platforms,
and use action mode for M and later platforms.

Bug: 28008144
Change-Id: Iae6e4d4f36c9f46f597f9e95cfdc45a1fc8a4186
parent 156de46c
Loading
Loading
Loading
Loading
+30 −13
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@ import android.animation.PropertyValuesHolder;
import android.app.ActionBar;
import android.app.Activity;
import android.content.ClipData;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
@@ -42,6 +43,7 @@ import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.ViewPager;
import android.text.Editable;
import android.text.SpannableStringBuilder;
@@ -369,14 +371,18 @@ public class Calculator extends Activity
            }

            if (mCurrentState == CalculatorState.ERROR) {
                final int errorColor = getColor(R.color.calculator_error_color);
                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(getColor(R.color.display_formula_text_color));
                mResultText.setTextColor(getColor(R.color.display_result_text_color));
                getWindow().setStatusBarColor(getColor(R.color.calculator_accent_color));
                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_accent_color));
            }

            invalidateOptionsMenu();
@@ -391,12 +397,15 @@ public class Calculator extends Activity
        }
    }

    // Stop any active ActionMode.  Return true if there was one.
    private boolean stopActionMode() {
        if (mResultText.stopActionMode()) {
    /**
     * Stop any active ActionMode or ContextMenu for copy/paste actions.
     * Return true if there was one.
     */
    private boolean stopActionModeOrContextMenu() {
        if (mResultText.stopActionModeOrContextMenu()) {
            return true;
        }
        if (mFormulaText.stopActionMode()) {
        if (mFormulaText.stopActionModeOrContextMenu()) {
            return true;
        }
        return false;
@@ -415,7 +424,7 @@ public class Calculator extends Activity

    @Override
    public void onBackPressed() {
        if (!stopActionMode()) {
        if (!stopActionModeOrContextMenu()) {
            if (mPadViewPager != null && mPadViewPager.getCurrentItem() != 0) {
                // Select the previous pad.
                mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
@@ -439,8 +448,8 @@ public class Calculator extends Activity
                return super.onKeyUp(keyCode, event);
        }

        // Stop the action mode if it's showing.
        stopActionMode();
        // Stop the action mode or context menu if it's showing.
        stopActionModeOrContextMenu();

        // Always cancel unrequested in-progress evaluation, so that we don't have to worry about
        // subsequent asynchronous completion.
@@ -615,7 +624,7 @@ public class Calculator extends Activity
    public void onButtonClick(View view) {
        // Any animation is ended before we get here.
        mCurrentButton = view;
        stopActionMode();
        stopActionModeOrContextMenu();

        // See onKey above for the rationale behind some of the behavior below:
        if (mCurrentState != CalculatorState.EVALUATE) {
@@ -811,7 +820,7 @@ public class Calculator extends Activity
        revealView.setBottom(displayRect.bottom);
        revealView.setLeft(displayRect.left);
        revealView.setRight(displayRect.right);
        revealView.setBackgroundColor(getColor(colorRes));
        revealView.setBackgroundColor(ContextCompat.getColor(this, colorRes));
        groupOverlay.add(revealView);

        final int[] clearLocation = new int[2];
@@ -1162,4 +1171,12 @@ public class Calculator extends Activity
        }
        return true;
    }

    /**
     * Clean up animation for context menu.
     */
    @Override
    public void onContextMenuClosed(Menu menu) {
        stopActionModeOrContextMenu();
    }
}
+145 −88
Original line number Diff line number Diff line
@@ -16,11 +16,14 @@

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 android.support.v4.content.ContextCompat;
import android.support.v4.os.BuildCompat;
import android.text.Layout;
import android.text.Spannable;
@@ -31,6 +34,7 @@ import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import android.util.AttributeSet;
import android.view.ActionMode;
import android.view.ContextMenu;
import android.view.GestureDetector;
import android.view.Menu;
import android.view.MenuInflater;
@@ -42,7 +46,7 @@ import android.widget.Toast;

// 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 {
public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenuItemClickListener {
    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
@@ -106,13 +110,19 @@ public class CalculatorResult extends AlignedTextView {
                            // 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 final ForegroundColorSpan mExponentColorSpan;
    private ActionMode.Callback mCopyActionModeCallback;
    private ContextMenu mContextMenu;

    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
@@ -126,7 +136,7 @@ public class CalculatorResult extends AlignedTextView {
                        mCurrentPos = mScroller.getFinalX();
                    }
                    mScroller.forceFinished(true);
                    stopActionMode();
                    stopActionModeOrContextMenu();
                    CalculatorResult.this.cancelLongPress();
                    // Ignore scrolls of error string, etc.
                    if (!mScrollable) return true;
@@ -143,7 +153,7 @@ public class CalculatorResult extends AlignedTextView {
                        mCurrentPos = mScroller.getFinalX();
                    }
                    mScroller.forceFinished(true);
                    stopActionMode();
                    stopActionModeOrContextMenu();
                    CalculatorResult.this.cancelLongPress();
                    if (!mScrollable) return true;
                    if (mCurrentPos + distance < mMinPos) {
@@ -170,24 +180,13 @@ public class CalculatorResult extends AlignedTextView {
                return mGestureDetector.onTouchEvent(event);
            }
        });
        setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                if (mValid) {
                    mActionMode = startActionMode(mCopyActionModeCallback,
                            ActionMode.TYPE_FLOATING);
                    return true;
                }
                return false;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            setupActionMode();
        } else {
            setupContextMenu();
        }
        });
        setHorizontallyScrolling(false);  // do it ourselves
        setCursorVisible(false);
        mExponentColorSpan = new ForegroundColorSpan(
                context.getColor(R.color.display_result_exponent_text_color));

        // Copy ActionMode is triggered explicitly, not through
        // setCustomSelectionActionModeCallback.
    }

    void setEvaluator(Evaluator evaluator) {
@@ -605,29 +604,17 @@ public class CalculatorResult extends AlignedTextView {
        }
    }

    // Copy support:

    private ActionMode.Callback2 mCopyActionModeCallback = new ActionMode.Callback2() {

        private BackgroundColorSpan mHighlightSpan;

        private void highlightResult() {
            final Spannable text = (Spannable) getText();
            mHighlightSpan = new BackgroundColorSpan(getHighlightColor());
            text.setSpan(mHighlightSpan, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }

        private void unhighlightResult() {
            final Spannable text = (Spannable) getText();
            text.removeSpan(mHighlightSpan);
        }
    /**
     * Use ActionMode for copy 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) {
            MenuInflater inflater = mode.getMenuInflater();
            inflater.inflate(R.menu.copy, menu);
            highlightResult();
            return true;
                final MenuInflater inflater = mode.getMenuInflater();
                return createCopyMenu(inflater, menu);
            }

            @Override
@@ -637,17 +624,10 @@ public class CalculatorResult extends AlignedTextView {

            @Override
            public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            switch (item.getItemId()) {
            case R.id.menu_copy:
                if (mEvaluator.reevaluationInProgress()) {
                    // Refuse to copy placeholder characters.
                    return false;
                } else {
                    copyContent();
                if (onMenuItemClick(item)) {
                    mode.finish();
                    return true;
                }
            default:
                } else {
                    return false;
                }
            }
@@ -672,10 +652,11 @@ public class CalculatorResult extends AlignedTextView {
                }

                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.
                    // 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;
@@ -685,15 +666,75 @@ public class CalculatorResult extends AlignedTextView {
                }
            }
        };
        setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                if (mValid) {
                    mActionMode = startActionMode(mCopyActionModeCallback,
                            ActionMode.TYPE_FLOATING);
                    return true;
                }
                return false;
            }
        });
    }

    public boolean stopActionMode() {
    /**
     * Use ContextMenu for copy 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());
                createCopyMenu(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 createCopyMenu(MenuInflater inflater, Menu menu) {
        inflater.inflate(R.menu.copy, menu);
        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);
@@ -713,4 +754,20 @@ public class CalculatorResult extends AlignedTextView {
        Toast.makeText(getContext(), R.string.text_copied_toast, Toast.LENGTH_SHORT).show();
    }

    @Override
    public boolean onMenuItemClick(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.menu_copy:
                if (mEvaluator.reevaluationInProgress()) {
                    // Refuse to copy placeholder characters.
                    return false;
                } else {
                    copyContent();
                    unhighlightResult();
                    return true;
                }
            default:
                return false;
        }
    }
}
+114 −60
Original line number Diff line number Diff line
@@ -16,16 +16,19 @@

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.util.AttributeSet;
import android.util.TypedValue;
import android.view.ActionMode;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -35,58 +38,10 @@ import android.widget.TextView;
/**
 * TextView adapted for Calculator display.
 */
public class CalculatorText extends AlignedTextView implements View.OnLongClickListener {
public class CalculatorText extends AlignedTextView implements MenuItem.OnMenuItemClickListener {

    public static final String TAG_ACTION_MODE = "ACTION_MODE";

    private final ActionMode.Callback2 mPasteActionModeCallback = new ActionMode.Callback2() {

        @Override
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            if (item.getItemId() == R.id.menu_paste) {
                paste();
                mode.finish();
                return true;
            }
            return false;
        }

        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            mode.setTag(TAG_ACTION_MODE);
            final ClipboardManager clipboard = (ClipboardManager) getContext()
                    .getSystemService(Context.CLIPBOARD_SERVICE);
            if (clipboard.hasPrimaryClip()) {
                bringPointIntoView(length());
                MenuInflater inflater = mode.getMenuInflater();
                inflater.inflate(R.menu.paste, menu);
                return true;
            }
            // Prevents the selection action mode on double tap.
            return false;
        }

        @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 towards the right, possibly over formula.
            outRect.left = outRect.right;
        }
    };

    // Temporary paint for use in layout methods.
    private final TextPaint mTempPaint = new TextPaint();

@@ -95,9 +50,9 @@ public class CalculatorText extends AlignedTextView implements View.OnLongClickL
    private final float mStepTextSize;

    private int mWidthConstraint = -1;

    private ActionMode mActionMode;

    private ActionMode.Callback mPasteActionModeCallback;
    private ContextMenu mContextMenu;
    private OnPasteListener mOnPasteListener;
    private OnTextSizeChangeListener mOnTextSizeChangeListener;

@@ -122,14 +77,11 @@ public class CalculatorText extends AlignedTextView implements View.OnLongClickL
                (mMaximumTextSize - mMinimumTextSize) / 3);
        a.recycle();

        // Add a long click to start the ActionMode manually.
        setOnLongClickListener(this);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            setupActionMode();
        } else {
            setupContextMenu();
        }

    @Override
    public boolean onLongClick(View v) {
        mActionMode = startActionMode(mPasteActionModeCallback, ActionMode.TYPE_FLOATING);
        return true;
    }

    @Override
@@ -253,11 +205,15 @@ public class CalculatorText extends AlignedTextView implements View.OnLongClickL
        setText(newText);
    }

    public boolean stopActionMode() {
    public boolean stopActionModeOrContextMenu() {
        if (mActionMode != null) {
            mActionMode.finish();
            return true;
        }
        if (mContextMenu != null) {
            mContextMenu.close();
            return true;
        }
        return false;
    }

@@ -269,6 +225,95 @@ public class CalculatorText extends AlignedTextView implements View.OnLongClickL
        mOnPasteListener = 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 createPasteMenu(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 towards the right, possibly over formula.
                outRect.left = outRect.right;
            }
        };
        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());
                createPasteMenu(inflater, contextMenu);
                mContextMenu = contextMenu;
                for(int i = 0; i < contextMenu.size(); i++) {
                    contextMenu.getItem(i).setOnMenuItemClickListener(CalculatorText.this);
                }
            }
        });
        setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                return showContextMenu();
            }
        });
    }

    private boolean createPasteMenu(MenuInflater inflater, Menu menu) {
        final ClipboardManager clipboard = (ClipboardManager) getContext()
                .getSystemService(Context.CLIPBOARD_SERVICE);
        if (clipboard.hasPrimaryClip()) {
            bringPointIntoView(length());
            inflater.inflate(R.menu.paste, menu);
            return true;
        }
        // Prevents the selection action mode on double tap.
        return false;
    }

    private void paste() {
        final ClipboardManager clipboard = (ClipboardManager) getContext()
                .getSystemService(Context.CLIPBOARD_SERVICE);
@@ -278,6 +323,15 @@ public class CalculatorText extends AlignedTextView implements View.OnLongClickL
        }
    }

    @Override
    public boolean onMenuItemClick(MenuItem item) {
        if (item.getItemId() == R.id.menu_paste) {
            paste();
            return true;
        }
        return false;
    }

    public interface OnTextSizeChangeListener {
        void onTextSizeChanged(TextView textView, float oldSize);
    }