1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.calculator2;
18
19import android.content.ClipData;
20import android.content.ClipDescription;
21import android.content.ClipboardManager;
22import android.content.Context;
23import android.graphics.Rect;
24import android.text.Layout;
25import android.text.Spannable;
26import android.text.SpannableString;
27import android.text.Spanned;
28import android.text.TextPaint;
29import android.text.style.BackgroundColorSpan;
30import android.text.style.ForegroundColorSpan;
31import android.util.AttributeSet;
32import android.view.ActionMode;
33import android.view.GestureDetector;
34import android.view.Menu;
35import android.view.MenuInflater;
36import android.view.MenuItem;
37import android.view.MotionEvent;
38import android.view.View;
39import android.widget.OverScroller;
40import android.widget.Toast;
41
42// A text widget that is "infinitely" scrollable to the right,
43// and obtains the text to display via a callback to Logic.
44public class CalculatorResult extends AlignedTextView {
45    static final int MAX_RIGHT_SCROLL = 10000000;
46    static final int INVALID = MAX_RIGHT_SCROLL + 10000;
47        // A larger value is unlikely to avoid running out of space
48    final OverScroller mScroller;
49    final GestureDetector mGestureDetector;
50    class MyTouchListener implements View.OnTouchListener {
51        @Override
52        public boolean onTouch(View v, MotionEvent event) {
53            return mGestureDetector.onTouchEvent(event);
54        }
55    }
56    final MyTouchListener mTouchListener = new MyTouchListener();
57    private Evaluator mEvaluator;
58    private boolean mScrollable = false;
59                            // A scrollable result is currently displayed.
60    private boolean mValid = false;
61                            // The result holds something valid; either a a number or an error
62                            // message.
63    // A suffix of "Pos" denotes a pixel offset.  Zero represents a scroll position
64    // in which the decimal point is just barely visible on the right of the display.
65    private int mCurrentPos;// Position of right of display relative to decimal point, in pixels.
66                            // Large positive values mean the decimal point is scrolled off the
67                            // left of the display.  Zero means decimal point is barely displayed
68                            // on the right.
69    private int mLastPos;   // Position already reflected in display. Pixels.
70    private int mMinPos;    // Minimum position before all digits disappear off the right. Pixels.
71    private int mMaxPos;    // Maximum position before we start displaying the infinite
72                            // sequence of trailing zeroes on the right. Pixels.
73    // In the following, we use a suffix of Offset to denote a character position in a numeric
74    // string relative to the decimal point.  Positive is to the right and negative is to
75    // the left. 1 = tenths position, -1 = units.  Integer.MAX_VALUE is sometimes used
76    // for the offset of the last digit in an a nonterminating decimal expansion.
77    // We use the suffix "Index" to denote a zero-based index into a string representing a
78    // result.
79    // TODO: Apply the same convention to other classes.
80    private int mMaxCharOffset;  // Character offset from decimal point of rightmost digit
81                                 // that should be displayed.  Essentially the same as
82    private int mLsdOffset;      // Position of least-significant digit in result
83    private int mLastDisplayedOffset; // Offset of last digit actually displayed after adding
84                                      // exponent.
85    private final Object mWidthLock = new Object();
86                            // Protects the next two fields.
87    private int mWidthConstraint = -1;
88                            // Our total width in pixels minus space for ellipsis.
89    private float mCharWidth = 1;
90                            // Maximum character width. For now we pretend that all characters
91                            // have this width.
92                            // TODO: We're not really using a fixed width font.  But it appears
93                            // to be close enough for the characters we use that the difference
94                            // is not noticeable.
95    private static final int MAX_WIDTH = 100;
96                            // Maximum number of digits displayed
97    public static final int MAX_LEADING_ZEROES = 6;
98                            // Maximum number of leading zeroes after decimal point before we
99                            // switch to scientific notation with negative exponent.
100    public static final int MAX_TRAILING_ZEROES = 6;
101                            // Maximum number of trailing zeroes before the decimal point before
102                            // we switch to scientific notation with positive exponent.
103    private static final int SCI_NOTATION_EXTRA = 1;
104                            // Extra digits for standard scientific notation.  In this case we
105                            // have a decimal point and no ellipsis.
106                            // We assume that we do not drop digits to make room for the decimal
107                            // point in ordinary scientific notation. Thus >= 1.
108    private ActionMode mActionMode;
109    private final ForegroundColorSpan mExponentColorSpan;
110
111    public CalculatorResult(Context context, AttributeSet attrs) {
112        super(context, attrs);
113        mScroller = new OverScroller(context);
114        mGestureDetector = new GestureDetector(context,
115            new GestureDetector.SimpleOnGestureListener() {
116                @Override
117                public boolean onDown(MotionEvent e) {
118                    return true;
119                }
120                @Override
121                public boolean onFling(MotionEvent e1, MotionEvent e2,
122                                       float velocityX, float velocityY) {
123                    if (!mScroller.isFinished()) {
124                        mCurrentPos = mScroller.getFinalX();
125                    }
126                    mScroller.forceFinished(true);
127                    stopActionMode();
128                    CalculatorResult.this.cancelLongPress();
129                    // Ignore scrolls of error string, etc.
130                    if (!mScrollable) return true;
131                    mScroller.fling(mCurrentPos, 0, - (int) velocityX, 0  /* horizontal only */,
132                                    mMinPos, mMaxPos, 0, 0);
133                    postInvalidateOnAnimation();
134                    return true;
135                }
136                @Override
137                public boolean onScroll(MotionEvent e1, MotionEvent e2,
138                                        float distanceX, float distanceY) {
139                    int distance = (int)distanceX;
140                    if (!mScroller.isFinished()) {
141                        mCurrentPos = mScroller.getFinalX();
142                    }
143                    mScroller.forceFinished(true);
144                    stopActionMode();
145                    CalculatorResult.this.cancelLongPress();
146                    if (!mScrollable) return true;
147                    if (mCurrentPos + distance < mMinPos) {
148                        distance = mMinPos - mCurrentPos;
149                    } else if (mCurrentPos + distance > mMaxPos) {
150                        distance = mMaxPos - mCurrentPos;
151                    }
152                    int duration = (int)(e2.getEventTime() - e1.getEventTime());
153                    if (duration < 1 || duration > 100) duration = 10;
154                    mScroller.startScroll(mCurrentPos, 0, distance, 0, (int)duration);
155                    postInvalidateOnAnimation();
156                    return true;
157                }
158                @Override
159                public void onLongPress(MotionEvent e) {
160                    if (mValid) {
161                        mActionMode = startActionMode(mCopyActionModeCallback,
162                                ActionMode.TYPE_FLOATING);
163                    }
164                }
165            });
166        setOnTouchListener(mTouchListener);
167        setHorizontallyScrolling(false);  // do it ourselves
168        setCursorVisible(false);
169        mExponentColorSpan = new ForegroundColorSpan(
170                context.getColor(R.color.display_result_exponent_text_color));
171
172        // Copy ActionMode is triggered explicitly, not through
173        // setCustomSelectionActionModeCallback.
174    }
175
176    void setEvaluator(Evaluator evaluator) {
177        mEvaluator = evaluator;
178    }
179
180    @Override
181    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
182        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
183
184        final TextPaint paint = getPaint();
185        final Context context = getContext();
186        final float newCharWidth = Layout.getDesiredWidth("\u2007", paint);
187        // Digits are presumed to have no more than newCharWidth.
188        // We sometimes replace a character by an ellipsis or, due to SCI_NOTATION_EXTRA, add
189        // an extra decimal separator beyond the maximum number of characters we normally allow.
190        // Empirically, our minus sign is also slightly wider than a digit, so we have to
191        // account for that.  We never have both an ellipsis and two minus signs, and
192        // we assume an ellipsis is no narrower than a minus sign.
193        final float decimalSeparatorWidth = Layout.getDesiredWidth(
194                context.getString(R.string.dec_point), paint);
195        final float minusExtraWidth = Layout.getDesiredWidth(
196                context.getString(R.string.op_sub), paint) - newCharWidth;
197        final float ellipsisExtraWidth = Layout.getDesiredWidth(KeyMaps.ELLIPSIS, paint)
198                - newCharWidth;
199        final int extraWidth = (int) (Math.ceil(Math.max(decimalSeparatorWidth + minusExtraWidth,
200                ellipsisExtraWidth)) + Math.max(minusExtraWidth, 0.0f));
201        final int newWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
202                - (getPaddingLeft() + getPaddingRight()) - extraWidth;
203        synchronized(mWidthLock) {
204            mWidthConstraint = newWidthConstraint;
205            mCharWidth = newCharWidth;
206        }
207    }
208
209    // Return the length of the exponent representation for the given exponent, in
210    // characters.
211    private final int expLen(int exp) {
212        if (exp == 0) return 0;
213        final int abs_exp_digits = (int) Math.ceil(Math.log10(Math.abs((double)exp))
214                + 0.0000000001d /* Round whole numbers to next integer */);
215        return abs_exp_digits + (exp >= 0 ? 1 : 2);
216    }
217
218    /**
219     * Initiate display of a new result.
220     * The parameters specify various properties of the result.
221     * @param initPrec Initial display precision computed by evaluator. (1 = tenths digit)
222     * @param msd Position of most significant digit.  Offset from left of string.
223                  Evaluator.INVALID_MSD if unknown.
224     * @param leastDigPos Position of least significant digit (1 = tenths digit)
225     *                    or Integer.MAX_VALUE.
226     * @param truncatedWholePart Result up to but not including decimal point.
227                                 Currently we only use the length.
228     */
229    void displayResult(int initPrec, int msd, int leastDigPos, String truncatedWholePart) {
230        initPositions(initPrec, msd, leastDigPos, truncatedWholePart);
231        redisplay();
232    }
233
234    /**
235     * Set up scroll bounds (mMinPos, mMaxPos, etc.) and determine whether the result is
236     * scrollable, based on the supplied information about the result.
237     * This is unfortunately complicated because we need to predict whether trailing digits
238     * will eventually be replaced by an exponent.
239     * Just appending the exponent during formatting would be simpler, but would produce
240     * jumpier results during transitions.
241     */
242    private void initPositions(int initPrecOffset, int msdIndex, int lsdOffset,
243            String truncatedWholePart) {
244        float charWidth;
245        int maxChars = getMaxChars();
246        mLastPos = INVALID;
247        mLsdOffset = lsdOffset;
248        synchronized(mWidthLock) {
249            charWidth = mCharWidth;
250        }
251        mCurrentPos = mMinPos = (int) Math.round(initPrecOffset * charWidth);
252        // Prevent scrolling past initial position, which is calculated to show leading digits.
253        if (msdIndex == Evaluator.INVALID_MSD) {
254            // Possible zero value
255            if (lsdOffset == Integer.MIN_VALUE) {
256                // Definite zero value.
257                mMaxPos = mMinPos;
258                mMaxCharOffset = (int) Math.round(mMaxPos/charWidth);
259                mScrollable = false;
260            } else {
261                // May be very small nonzero value.  Allow user to find out.
262                mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL;
263                mMinPos -= charWidth;  // Allow for future minus sign.
264                mScrollable = true;
265            }
266            return;
267        }
268        int wholeLen =  truncatedWholePart.length();
269        int negative = truncatedWholePart.charAt(0) == '-' ? 1 : 0;
270        if (msdIndex > wholeLen && msdIndex <= wholeLen + 3) {
271            // Avoid tiny negative exponent; pretend msdIndex is just to the right of decimal point.
272            msdIndex = wholeLen - 1;
273        }
274        int minCharOffset = msdIndex - wholeLen;
275                                // Position of leftmost significant digit relative to dec. point.
276                                // Usually negative.
277        mMaxCharOffset = MAX_RIGHT_SCROLL; // How far does it make sense to scroll right?
278        // If msd is left of decimal point should logically be
279        // mMinPos = - (int) Math.ceil(getPaint().measureText(truncatedWholePart)), but
280        // we eventually translate to a character position by dividing by mCharWidth.
281        // To avoid rounding issues, we use the analogous computation here.
282        if (minCharOffset > -1 && minCharOffset < MAX_LEADING_ZEROES + 2) {
283            // Small number of leading zeroes, avoid scientific notation.
284            minCharOffset = -1;
285        }
286        if (lsdOffset < MAX_RIGHT_SCROLL) {
287            mMaxCharOffset = lsdOffset;
288            if (mMaxCharOffset < -1 && mMaxCharOffset > -(MAX_TRAILING_ZEROES + 2)) {
289                mMaxCharOffset = -1;
290            }
291            // lsdOffset is positive or negative, never 0.
292            int currentExpLen = 0;  // Length of required standard scientific notation exponent.
293            if (mMaxCharOffset < -1) {
294                currentExpLen = expLen(-minCharOffset - 1);
295            } else if (minCharOffset > -1 || mMaxCharOffset >= maxChars) {
296                // Number either entirely to the right of decimal point, or decimal point not
297                // visible when scrolled to the right.
298                currentExpLen = expLen(-minCharOffset);
299            }
300            mScrollable = (mMaxCharOffset + currentExpLen - minCharOffset + negative >= maxChars);
301            int newMaxCharOffset;
302            if (currentExpLen > 0) {
303                if (mScrollable) {
304                    // We'll use exponent corresponding to leastDigPos when scrolled to right.
305                    newMaxCharOffset = mMaxCharOffset + expLen(-lsdOffset);
306                } else {
307                    newMaxCharOffset = mMaxCharOffset + currentExpLen;
308                }
309                if (mMaxCharOffset <= -1 && newMaxCharOffset > -1) {
310                    // Very unlikely; just drop exponent.
311                    mMaxCharOffset = -1;
312                } else {
313                    mMaxCharOffset = newMaxCharOffset;
314                }
315            }
316            mMaxPos = Math.min((int) Math.round(mMaxCharOffset * charWidth), MAX_RIGHT_SCROLL);
317            if (!mScrollable) {
318                // Position the number consistently with our assumptions to make sure it
319                // actually fits.
320                mCurrentPos = mMaxPos;
321            }
322        } else {
323            mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL;
324            mScrollable = true;
325        }
326    }
327
328    void displayError(int resourceId) {
329        mValid = true;
330        mScrollable = false;
331        setText(resourceId);
332    }
333
334    private final int MAX_COPY_SIZE = 1000000;
335
336    /*
337     * Return the most significant digit position in the given string or Evaluator.INVALID_MSD.
338     * Unlike Evaluator.getMsdIndexOf, we treat a final 1 as significant.
339     */
340    public static int getNaiveMsdIndexOf(String s) {
341        int len = s.length();
342        for (int i = 0; i < len; ++i) {
343            char c = s.charAt(i);
344            if (c != '-' && c != '.' && c != '0') {
345                return i;
346            }
347        }
348        return Evaluator.INVALID_MSD;
349    }
350
351    // Format a result returned by Evaluator.getString() into a single line containing ellipses
352    // (if appropriate) and an exponent (if appropriate).  precOffset is the value that was passed
353    // to getString and thus identifies the significance of the rightmost digit.
354    // A value of 1 means the rightmost digits corresponds to tenths.
355    // maxDigs is the maximum number of characters in the result.
356    // We set lastDisplayedOffset[0] to the offset of the last digit actually appearing in
357    // the display.
358    // If forcePrecision is true, we make sure that the last displayed digit corresponds to
359    // precOffset, and allow maxDigs to be exceeded in assing the exponent.
360    // We add two distinct kinds of exponents:
361    // (1) If the final result contains the leading digit we use standard scientific notation.
362    // (2) If not, we add an exponent corresponding to an interpretation of the final result as
363    //     an integer.
364    // We add an ellipsis on the left if the result was truncated.
365    // We add ellipses and exponents in a way that leaves most digits in the position they
366    // would have been in had we not done so.
367    // This minimizes jumps as a result of scrolling.  Result is NOT internationalized,
368    // uses "E" for exponent.
369    public String formatResult(String in, int precOffset, int maxDigs, boolean truncated,
370            boolean negative, int lastDisplayedOffset[], boolean forcePrecision) {
371        final int minusSpace = negative ? 1 : 0;
372        final int msdIndex = truncated ? -1 : getNaiveMsdIndexOf(in);  // INVALID_MSD is OK.
373        String result = in;
374        if (truncated || (negative && result.charAt(0) != '-')) {
375            result = KeyMaps.ELLIPSIS + result.substring(1, result.length());
376            // Ellipsis may be removed again in the type(1) scientific notation case.
377        }
378        final int decIndex = result.indexOf('.');
379        lastDisplayedOffset[0] = precOffset;
380        if ((decIndex == -1 || msdIndex != Evaluator.INVALID_MSD
381                && msdIndex - decIndex > MAX_LEADING_ZEROES + 1) &&  precOffset != -1) {
382            // No decimal point displayed, and it's not just to the right of the last digit,
383            // or we should suppress leading zeroes.
384            // Add an exponent to let the user track which digits are currently displayed.
385            // Start with type (2) exponent if we dropped no digits. -1 accounts for decimal point.
386            final int initExponent = precOffset > 0 ? -precOffset : -precOffset - 1;
387            int exponent = initExponent;
388            boolean hasPoint = false;
389            if (!truncated && msdIndex < maxDigs - 1
390                    && result.length() - msdIndex + 1 + minusSpace
391                    <= maxDigs + SCI_NOTATION_EXTRA) {
392                // Type (1) exponent computation and transformation:
393                // Leading digit is in display window. Use standard calculator scientific notation
394                // with one digit to the left of the decimal point. Insert decimal point and
395                // delete leading zeroes.
396                // We try to keep leading digits roughly in position, and never
397                // lengthen the result by more than SCI_NOTATION_EXTRA.
398                final int resLen = result.length();
399                String fraction = result.substring(msdIndex + 1, resLen);
400                result = (negative ? "-" : "") + result.substring(msdIndex, msdIndex + 1)
401                        + "." + fraction;
402                // Original exp was correct for decimal point at right of fraction.
403                // Adjust by length of fraction.
404                exponent = initExponent + resLen - msdIndex - 1;
405                hasPoint = true;
406            }
407            // Exponent can't be zero.
408            // Actually add the exponent of either type:
409            if (!forcePrecision) {
410                int dropDigits;  // Digits to drop to make room for exponent.
411                if (hasPoint) {
412                    // Type (1) exponent.
413                    // Drop digits even if there is room. Otherwise the scrolling gets jumpy.
414                    dropDigits = expLen(exponent);
415                    if (dropDigits >= result.length() - 1) {
416                        // Jumpy is better than no mantissa.  Probably impossible anyway.
417                        dropDigits = Math.max(result.length() - 2, 0);
418                    }
419                } else {
420                    // Type (2) exponent.
421                    // Exponent depends on the number of digits we drop, which depends on
422                    // exponent ...
423                    for (dropDigits = 2; expLen(initExponent + dropDigits) > dropDigits;
424                            ++dropDigits) {}
425                    exponent = initExponent + dropDigits;
426                    if (precOffset - dropDigits > mLsdOffset) {
427                        // This can happen if e.g. result = 10^40 + 10^10
428                        // It turns out we would otherwise display ...10e9 because it takes
429                        // the same amount of space as ...1e10 but shows one more digit.
430                        // But we don't want to display a trailing zero, even if it's free.
431                        ++dropDigits;
432                        ++exponent;
433                    }
434                }
435                result = result.substring(0, result.length() - dropDigits);
436                lastDisplayedOffset[0] -= dropDigits;
437            }
438            result = result + "E" + Integer.toString(exponent);
439        }
440        return result;
441    }
442
443    /**
444     * Get formatted, but not internationalized, result from mEvaluator.
445     * @param precOffset requested position (1 = tenths) of last included digit.
446     * @param maxSize Maximum number of characters (more or less) in result.
447     * @param lastDisplayedOffset Zeroth entry is set to actual offset of last included digit,
448     *                            after adjusting for exponent, etc.
449     * @param forcePrecision Ensure that last included digit is at pos, at the expense
450     *                       of treating maxSize as a soft limit.
451     */
452    private String getFormattedResult(int precOffset, int maxSize, int lastDisplayedOffset[],
453            boolean forcePrecision) {
454        final boolean truncated[] = new boolean[1];
455        final boolean negative[] = new boolean[1];
456        final int requestedPrecOffset[] = {precOffset};
457        final String rawResult = mEvaluator.getString(requestedPrecOffset, mMaxCharOffset,
458                maxSize, truncated, negative);
459        return formatResult(rawResult, requestedPrecOffset[0], maxSize, truncated[0], negative[0],
460                lastDisplayedOffset, forcePrecision);
461   }
462
463    // Return entire result (within reason) up to current displayed precision.
464    public String getFullText() {
465        if (!mValid) return "";
466        if (!mScrollable) return getText().toString();
467        int currentCharOffset = getCurrentCharOffset();
468        int unused[] = new int[1];
469        return KeyMaps.translateResult(getFormattedResult(mLastDisplayedOffset, MAX_COPY_SIZE,
470                unused, true));
471    }
472
473    public boolean fullTextIsExact() {
474        return !mScrollable
475                || mMaxCharOffset == getCurrentCharOffset() && mMaxCharOffset != MAX_RIGHT_SCROLL;
476    }
477
478    /**
479     * Return the maximum number of characters that will fit in the result display.
480     * May be called asynchronously from non-UI thread.
481     */
482    int getMaxChars() {
483        int result;
484        synchronized(mWidthLock) {
485            result = (int) Math.floor(mWidthConstraint / mCharWidth);
486            // We can apparently finish evaluating before onMeasure in CalculatorText has been
487            // called, in which case we get 0 or -1 as the width constraint.
488        }
489        if (result <= 0) {
490            // Return something conservatively big, to force sufficient evaluation.
491            return MAX_WIDTH;
492        } else {
493            return result;
494        }
495    }
496
497    /**
498     * @return {@code true} if the currently displayed result is scrollable
499     */
500    public boolean isScrollable() {
501        return mScrollable;
502    }
503
504    int getCurrentCharOffset() {
505        synchronized(mWidthLock) {
506            return (int) Math.round(mCurrentPos / mCharWidth);
507        }
508    }
509
510    void clear() {
511        mValid = false;
512        mScrollable = false;
513        setText("");
514    }
515
516    void redisplay() {
517        int currentCharOffset = getCurrentCharOffset();
518        int maxChars = getMaxChars();
519        int lastDisplayedOffset[] = new int[1];
520        String result = getFormattedResult(currentCharOffset, maxChars, lastDisplayedOffset, false);
521        int expIndex = result.indexOf('E');
522        result = KeyMaps.translateResult(result);
523        if (expIndex > 0 && result.indexOf('.') == -1) {
524          // Gray out exponent if used as position indicator
525            SpannableString formattedResult = new SpannableString(result);
526            formattedResult.setSpan(mExponentColorSpan, expIndex, result.length(),
527                                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
528            setText(formattedResult);
529        } else {
530            setText(result);
531        }
532        mLastDisplayedOffset = lastDisplayedOffset[0];
533        mValid = true;
534    }
535
536    @Override
537    public void computeScroll() {
538        if (!mScrollable) return;
539        if (mScroller.computeScrollOffset()) {
540            mCurrentPos = mScroller.getCurrX();
541            if (mCurrentPos != mLastPos) {
542                mLastPos = mCurrentPos;
543                redisplay();
544            }
545            if (!mScroller.isFinished()) {
546                postInvalidateOnAnimation();
547            }
548        }
549    }
550
551    // Copy support:
552
553    private ActionMode.Callback2 mCopyActionModeCallback = new ActionMode.Callback2() {
554
555        private BackgroundColorSpan mHighlightSpan;
556
557        private void highlightResult() {
558            final Spannable text = (Spannable) getText();
559            mHighlightSpan = new BackgroundColorSpan(getHighlightColor());
560            text.setSpan(mHighlightSpan, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
561        }
562
563        private void unhighlightResult() {
564            final Spannable text = (Spannable) getText();
565            text.removeSpan(mHighlightSpan);
566        }
567
568        @Override
569        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
570            MenuInflater inflater = mode.getMenuInflater();
571            inflater.inflate(R.menu.copy, menu);
572            highlightResult();
573            return true;
574        }
575
576        @Override
577        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
578            return false; // Return false if nothing is done
579        }
580
581        @Override
582        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
583            switch (item.getItemId()) {
584            case R.id.menu_copy:
585                copyContent();
586                mode.finish();
587                return true;
588            default:
589                return false;
590            }
591        }
592
593        @Override
594        public void onDestroyActionMode(ActionMode mode) {
595            unhighlightResult();
596            mActionMode = null;
597        }
598
599        @Override
600        public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
601            super.onGetContentRect(mode, view, outRect);
602            outRect.left += getPaddingLeft();
603            outRect.top += getPaddingTop();
604            outRect.right -= getPaddingRight();
605            outRect.bottom -= getPaddingBottom();
606            final int width = (int) Layout.getDesiredWidth(getText(), getPaint());
607            if (width < outRect.width()) {
608                outRect.left = outRect.right - width;
609            }
610        }
611    };
612
613    public boolean stopActionMode() {
614        if (mActionMode != null) {
615            mActionMode.finish();
616            return true;
617        }
618        return false;
619    }
620
621    private void setPrimaryClip(ClipData clip) {
622        ClipboardManager clipboard = (ClipboardManager) getContext().
623                                               getSystemService(Context.CLIPBOARD_SERVICE);
624        clipboard.setPrimaryClip(clip);
625    }
626
627    private void copyContent() {
628        final CharSequence text = getFullText();
629        ClipboardManager clipboard =
630                (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
631        // We include a tag URI, to allow us to recognize our own results and handle them
632        // specially.
633        ClipData.Item newItem = new ClipData.Item(text, null, mEvaluator.capture());
634        String[] mimeTypes = new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN};
635        ClipData cd = new ClipData("calculator result", mimeTypes, newItem);
636        clipboard.setPrimaryClip(cd);
637        Toast.makeText(getContext(), R.string.text_copied_toast, Toast.LENGTH_SHORT).show();
638    }
639
640}
641