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 
17 package com.android.calculator2;
18 
19 import android.content.ClipData;
20 import android.content.ClipDescription;
21 import android.content.ClipboardManager;
22 import android.content.Context;
23 import android.graphics.Rect;
24 import android.text.Layout;
25 import android.text.Spannable;
26 import android.text.SpannableString;
27 import android.text.Spanned;
28 import android.text.TextPaint;
29 import android.text.style.BackgroundColorSpan;
30 import android.text.style.ForegroundColorSpan;
31 import android.util.AttributeSet;
32 import android.view.ActionMode;
33 import android.view.GestureDetector;
34 import android.view.Menu;
35 import android.view.MenuInflater;
36 import android.view.MenuItem;
37 import android.view.MotionEvent;
38 import android.view.View;
39 import android.widget.OverScroller;
40 import 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.
44 public 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
onTouch(View v, MotionEvent event)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 
CalculatorResult(Context context, AttributeSet attrs)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 
setEvaluator(Evaluator evaluator)176     void setEvaluator(Evaluator evaluator) {
177         mEvaluator = evaluator;
178     }
179 
180     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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.
expLen(int exp)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      */
displayResult(int initPrec, int msd, int leastDigPos, String truncatedWholePart)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      */
initPositions(int initPrecOffset, int msdIndex, int lsdOffset, String truncatedWholePart)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 
displayError(int resourceId)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      */
getNaiveMsdIndexOf(String s)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.
formatResult(String in, int precOffset, int maxDigs, boolean truncated, boolean negative, int lastDisplayedOffset[], boolean forcePrecision)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      */
getFormattedResult(int precOffset, int maxSize, int lastDisplayedOffset[], boolean forcePrecision)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.
getFullText()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 
fullTextIsExact()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      */
getMaxChars()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      */
isScrollable()500     public boolean isScrollable() {
501         return mScrollable;
502     }
503 
getCurrentCharOffset()504     int getCurrentCharOffset() {
505         synchronized(mWidthLock) {
506             return (int) Math.round(mCurrentPos / mCharWidth);
507         }
508     }
509 
clear()510     void clear() {
511         mValid = false;
512         mScrollable = false;
513         setText("");
514     }
515 
redisplay()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
computeScroll()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 
stopActionMode()613     public boolean stopActionMode() {
614         if (mActionMode != null) {
615             mActionMode.finish();
616             return true;
617         }
618         return false;
619     }
620 
setPrimaryClip(ClipData clip)621     private void setPrimaryClip(ClipData clip) {
622         ClipboardManager clipboard = (ClipboardManager) getContext().
623                                                getSystemService(Context.CLIPBOARD_SERVICE);
624         clipboard.setPrimaryClip(clip);
625     }
626 
copyContent()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