1 /*
2  * Copyright (C) 2016 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.annotation.TargetApi;
20 import android.content.ClipData;
21 import android.content.ClipDescription;
22 import android.content.ClipboardManager;
23 import android.content.Context;
24 import android.graphics.Rect;
25 import android.os.Build;
26 import android.support.annotation.IntDef;
27 import android.support.v4.content.ContextCompat;
28 import android.support.v4.os.BuildCompat;
29 import android.text.Layout;
30 import android.text.Spannable;
31 import android.text.SpannableString;
32 import android.text.Spanned;
33 import android.text.TextPaint;
34 import android.text.style.BackgroundColorSpan;
35 import android.text.style.ForegroundColorSpan;
36 import android.text.style.RelativeSizeSpan;
37 import android.util.AttributeSet;
38 import android.view.ActionMode;
39 import android.view.ContextMenu;
40 import android.view.GestureDetector;
41 import android.view.Menu;
42 import android.view.MenuInflater;
43 import android.view.MenuItem;
44 import android.view.MotionEvent;
45 import android.view.View;
46 import android.view.ViewConfiguration;
47 import android.widget.OverScroller;
48 import android.widget.Toast;
49 
50 import java.lang.annotation.Retention;
51 import java.lang.annotation.RetentionPolicy;
52 
53 // A text widget that is "infinitely" scrollable to the right,
54 // and obtains the text to display via a callback to Logic.
55 public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenuItemClickListener,
56         Evaluator.EvaluationListener, Evaluator.CharMetricsInfo {
57     static final int MAX_RIGHT_SCROLL = 10000000;
58     static final int INVALID = MAX_RIGHT_SCROLL + 10000;
59         // A larger value is unlikely to avoid running out of space
60     final OverScroller mScroller;
61     final GestureDetector mGestureDetector;
62     private long mIndex;  // Index of expression we are displaying.
63     private Evaluator mEvaluator;
64     private boolean mScrollable = false;
65                             // A scrollable result is currently displayed.
66     private boolean mValid = false;
67                             // The result holds a valid number (not an error message).
68     // A suffix of "Pos" denotes a pixel offset.  Zero represents a scroll position
69     // in which the decimal point is just barely visible on the right of the display.
70     private int mCurrentPos;// Position of right of display relative to decimal point, in pixels.
71                             // Large positive values mean the decimal point is scrolled off the
72                             // left of the display.  Zero means decimal point is barely displayed
73                             // on the right.
74     private int mLastPos;   // Position already reflected in display. Pixels.
75     private int mMinPos;    // Minimum position to avoid unnecessary blanks on the left. Pixels.
76     private int mMaxPos;    // Maximum position before we start displaying the infinite
77                             // sequence of trailing zeroes on the right. Pixels.
78     private int mWholeLen;  // Length of the whole part of current result.
79     // In the following, we use a suffix of Offset to denote a character position in a numeric
80     // string relative to the decimal point.  Positive is to the right and negative is to
81     // the left. 1 = tenths position, -1 = units.  Integer.MAX_VALUE is sometimes used
82     // for the offset of the last digit in an a nonterminating decimal expansion.
83     // We use the suffix "Index" to denote a zero-based index into a string representing a
84     // result.
85     private int mMaxCharOffset;  // Character offset from decimal point of rightmost digit
86                                  // that should be displayed, plus the length of any exponent
87                                  // needed to display that digit.
88                                  // Limited to MAX_RIGHT_SCROLL. Often the same as:
89     private int mLsdOffset;      // Position of least-significant digit in result
90     private int mLastDisplayedOffset; // Offset of last digit actually displayed after adding
91                                       // exponent.
92     private boolean mWholePartFits;  // Scientific notation not needed for initial display.
93     private float mNoExponentCredit;
94                             // Fraction of digit width saved by avoiding scientific notation.
95                             // Only accessed from UI thread.
96     private boolean mAppendExponent;
97                             // The result fits entirely in the display, even with an exponent,
98                             // but not with grouping separators. Since the result is not
99                             // scrollable, and we do not add the exponent to max. scroll position,
100                             // append an exponent insteadd of replacing trailing digits.
101     private final Object mWidthLock = new Object();
102                             // Protects the next five fields.  These fields are only
103                             // updated by the UI thread, and read accesses by the UI thread
104                             // sometimes do not acquire the lock.
105     private int mWidthConstraint = 0;
106                             // Our total width in pixels minus space for ellipsis.
107                             // 0 ==> uninitialized.
108     private float mCharWidth = 1;
109                             // Maximum character width. For now we pretend that all characters
110                             // have this width.
111                             // TODO: We're not really using a fixed width font.  But it appears
112                             // to be close enough for the characters we use that the difference
113                             // is not noticeable.
114     private float mGroupingSeparatorWidthRatio;
115                             // Fraction of digit width occupied by a digit separator.
116     private float mDecimalCredit;
117                             // Fraction of digit width saved by replacing digit with decimal point.
118     private float mNoEllipsisCredit;
119                             // Fraction of digit width saved by both replacing ellipsis with digit
120                             // and avoiding scientific notation.
121     @Retention(RetentionPolicy.SOURCE)
122     @IntDef({SHOULD_REQUIRE, SHOULD_EVALUATE, SHOULD_NOT_EVALUATE})
123     public @interface EvaluationRequest {}
124     public static final int SHOULD_REQUIRE = 2;
125     public static final int SHOULD_EVALUATE = 1;
126     public static final int SHOULD_NOT_EVALUATE = 0;
127     @EvaluationRequest private int mEvaluationRequest = SHOULD_REQUIRE;
128                             // Should we evaluate when layout completes, and how?
129     private Evaluator.EvaluationListener mEvaluationListener = this;
130                             // Listener to use if/when evaluation is requested.
131     public static final int MAX_LEADING_ZEROES = 6;
132                             // Maximum number of leading zeroes after decimal point before we
133                             // switch to scientific notation with negative exponent.
134     public static final int MAX_TRAILING_ZEROES = 6;
135                             // Maximum number of trailing zeroes before the decimal point before
136                             // we switch to scientific notation with positive exponent.
137     private static final int SCI_NOTATION_EXTRA = 1;
138                             // Extra digits for standard scientific notation.  In this case we
139                             // have a decimal point and no ellipsis.
140                             // We assume that we do not drop digits to make room for the decimal
141                             // point in ordinary scientific notation. Thus >= 1.
142     private static final int MAX_COPY_EXTRA = 100;
143                             // The number of extra digits we are willing to compute to copy
144                             // a result as an exact number.
145     private static final int MAX_RECOMPUTE_DIGITS = 2000;
146                             // The maximum number of digits we're willing to recompute in the UI
147                             // thread.  We only do this for known rational results, where we
148                             // can bound the computation cost.
149     private final ForegroundColorSpan mExponentColorSpan;
150     private final BackgroundColorSpan mHighlightSpan;
151 
152     private ActionMode mActionMode;
153     private ActionMode.Callback mCopyActionModeCallback;
154     private ContextMenu mContextMenu;
155 
156     // The user requested that the result currently being evaluated should be stored to "memory".
157     private boolean mStoreToMemoryRequested = false;
158 
CalculatorResult(Context context, AttributeSet attrs)159     public CalculatorResult(Context context, AttributeSet attrs) {
160         super(context, attrs);
161         mScroller = new OverScroller(context);
162         mHighlightSpan = new BackgroundColorSpan(getHighlightColor());
163         mExponentColorSpan = new ForegroundColorSpan(
164                 ContextCompat.getColor(context, R.color.display_result_exponent_text_color));
165         mGestureDetector = new GestureDetector(context,
166             new GestureDetector.SimpleOnGestureListener() {
167                 @Override
168                 public boolean onDown(MotionEvent e) {
169                     return true;
170                 }
171                 @Override
172                 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
173                         float velocityY) {
174                     if (!mScroller.isFinished()) {
175                         mCurrentPos = mScroller.getFinalX();
176                     }
177                     mScroller.forceFinished(true);
178                     stopActionModeOrContextMenu();
179                     CalculatorResult.this.cancelLongPress();
180                     // Ignore scrolls of error string, etc.
181                     if (!mScrollable) return true;
182                     mScroller.fling(mCurrentPos, 0, - (int) velocityX, 0  /* horizontal only */,
183                                     mMinPos, mMaxPos, 0, 0);
184                     postInvalidateOnAnimation();
185                     return true;
186                 }
187                 @Override
188                 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
189                         float distanceY) {
190                     int distance = (int)distanceX;
191                     if (!mScroller.isFinished()) {
192                         mCurrentPos = mScroller.getFinalX();
193                     }
194                     mScroller.forceFinished(true);
195                     stopActionModeOrContextMenu();
196                     CalculatorResult.this.cancelLongPress();
197                     if (!mScrollable) return true;
198                     if (mCurrentPos + distance < mMinPos) {
199                         distance = mMinPos - mCurrentPos;
200                     } else if (mCurrentPos + distance > mMaxPos) {
201                         distance = mMaxPos - mCurrentPos;
202                     }
203                     int duration = (int)(e2.getEventTime() - e1.getEventTime());
204                     if (duration < 1 || duration > 100) duration = 10;
205                     mScroller.startScroll(mCurrentPos, 0, distance, 0, (int)duration);
206                     postInvalidateOnAnimation();
207                     return true;
208                 }
209                 @Override
210                 public void onLongPress(MotionEvent e) {
211                     if (mValid) {
212                         performLongClick();
213                     }
214                 }
215             });
216 
217         final int slop = ViewConfiguration.get(context).getScaledTouchSlop();
218         setOnTouchListener(new View.OnTouchListener() {
219 
220             // Used to determine whether a touch event should be intercepted.
221             private float mInitialDownX;
222             private float mInitialDownY;
223 
224             @Override
225             public boolean onTouch(View v, MotionEvent event) {
226                 final int action = event.getActionMasked();
227 
228                 final float x = event.getX();
229                 final float y = event.getY();
230                 switch (action) {
231                     case MotionEvent.ACTION_DOWN:
232                         mInitialDownX = x;
233                         mInitialDownY = y;
234                         break;
235                     case MotionEvent.ACTION_MOVE:
236                         final float deltaX = Math.abs(x - mInitialDownX);
237                         final float deltaY = Math.abs(y - mInitialDownY);
238                         if (deltaX > slop && deltaX > deltaY) {
239                             // Prevent the DragLayout from intercepting horizontal scrolls.
240                             getParent().requestDisallowInterceptTouchEvent(true);
241                         }
242                 }
243                 return mGestureDetector.onTouchEvent(event);
244             }
245         });
246 
247         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
248             setupActionMode();
249         } else {
250             setupContextMenu();
251         }
252 
253         setCursorVisible(false);
254         setLongClickable(false);
255         setContentDescription(context.getString(R.string.desc_result));
256     }
257 
setEvaluator(Evaluator evaluator, long index)258     void setEvaluator(Evaluator evaluator, long index) {
259         mEvaluator = evaluator;
260         mIndex = index;
261         requestLayout();
262     }
263 
264     // Compute maximum digit width the hard way.
getMaxDigitWidth(TextPaint paint)265     private static float getMaxDigitWidth(TextPaint paint) {
266         // Compute the maximum advance width for each digit, thus accounting for between-character
267         // spaces. If we ever support other kinds of digits, we may have to avoid kerning effects
268         // that could reduce the advance width within this particular string.
269         final String allDigits = "0123456789";
270         final float[] widths = new float[allDigits.length()];
271         paint.getTextWidths(allDigits, widths);
272         float maxWidth = 0;
273         for (float x : widths) {
274             maxWidth = Math.max(x, maxWidth);
275         }
276         return maxWidth;
277     }
278 
279     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)280     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
281         if (!isLaidOut()) {
282             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
283             // Set a minimum height so scaled error messages won't affect our layout.
284             setMinimumHeight(getLineHeight() + getCompoundPaddingBottom()
285                     + getCompoundPaddingTop());
286         }
287 
288         final TextPaint paint = getPaint();
289         final Context context = getContext();
290         final float newCharWidth = getMaxDigitWidth(paint);
291         // Digits are presumed to have no more than newCharWidth.
292         // There are two instances when we know that the result is otherwise narrower than
293         // expected:
294         // 1. For standard scientific notation (our type 1), we know that we have a norrow decimal
295         // point and no (usually wide) ellipsis symbol. We allow one extra digit
296         // (SCI_NOTATION_EXTRA) to compensate, and consider that in determining available width.
297         // 2. If we are using digit grouping separators and a decimal point, we give ourselves
298         // a fractional extra space for those separators, the value of which depends on whether
299         // there is also an ellipsis.
300         //
301         // Maximum extra space we need in various cases:
302         // Type 1 scientific notation, assuming ellipsis, minus sign and E are wider than a digit:
303         //    Two minus signs + "E" + "." - 3 digits.
304         // Type 2 scientific notation:
305         //    Ellipsis + "E" + "-" - 3 digits.
306         // In the absence of scientific notation, we may need a little less space.
307         // We give ourselves a bit of extra credit towards comma insertion and give
308         // ourselves more if we have either
309         //    No ellipsis, or
310         //    A decimal separator.
311 
312         // Calculate extra space we need to reserve, in addition to character count.
313         final float decimalSeparatorWidth = Layout.getDesiredWidth(
314                 context.getString(R.string.dec_point), paint);
315         final float minusWidth = Layout.getDesiredWidth(context.getString(R.string.op_sub), paint);
316         final float minusExtraWidth = Math.max(minusWidth - newCharWidth, 0.0f);
317         final float ellipsisWidth = Layout.getDesiredWidth(KeyMaps.ELLIPSIS, paint);
318         final float ellipsisExtraWidth =  Math.max(ellipsisWidth - newCharWidth, 0.0f);
319         final float expWidth = Layout.getDesiredWidth(KeyMaps.translateResult("e"), paint);
320         final float expExtraWidth =  Math.max(expWidth - newCharWidth, 0.0f);
321         final float type1Extra = 2 * minusExtraWidth + expExtraWidth + decimalSeparatorWidth;
322         final float type2Extra = ellipsisExtraWidth + expExtraWidth + minusExtraWidth;
323         final float extraWidth = Math.max(type1Extra, type2Extra);
324         final int intExtraWidth = (int) Math.ceil(extraWidth) + 1 /* to cover rounding sins */;
325         final int newWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
326                 - (getPaddingLeft() + getPaddingRight()) - intExtraWidth;
327 
328         // Calculate other width constants we need to handle grouping separators.
329         final float groupingSeparatorW =
330                 Layout.getDesiredWidth(KeyMaps.translateResult(","), paint);
331         // Credits in the absence of any scientific notation:
332         float noExponentCredit = extraWidth - Math.max(ellipsisExtraWidth, minusExtraWidth);
333         final float noEllipsisCredit = extraWidth - minusExtraWidth;  // includes noExponentCredit.
334         final float decimalCredit = Math.max(newCharWidth - decimalSeparatorWidth, 0.0f);
335 
336         mNoExponentCredit = noExponentCredit / newCharWidth;
337         synchronized(mWidthLock) {
338             mWidthConstraint = newWidthConstraint;
339             mCharWidth = newCharWidth;
340             mNoEllipsisCredit = noEllipsisCredit / newCharWidth;
341             mDecimalCredit = decimalCredit / newCharWidth;
342             mGroupingSeparatorWidthRatio = groupingSeparatorW / newCharWidth;
343         }
344 
345         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
346     }
347 
348     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)349     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
350         super.onLayout(changed, left, top, right, bottom);
351 
352         if (mEvaluator != null && mEvaluationRequest != SHOULD_NOT_EVALUATE) {
353             final CalculatorExpr expr = mEvaluator.getExpr(mIndex);
354             if (expr != null && expr.hasInterestingOps()) {
355                 if (mEvaluationRequest == SHOULD_REQUIRE) {
356                     mEvaluator.requireResult(mIndex, mEvaluationListener, this);
357                 } else {
358                     mEvaluator.evaluateAndNotify(mIndex, mEvaluationListener, this);
359                 }
360             }
361         }
362     }
363 
364     /**
365      * Specify whether we should evaluate result on layout.
366      * @param should one of SHOULD_REQUIRE, SHOULD_EVALUATE, SHOULD_NOT_EVALUATE
367      */
setShouldEvaluateResult(@valuationRequest int request, Evaluator.EvaluationListener listener)368     public void setShouldEvaluateResult(@EvaluationRequest int request,
369             Evaluator.EvaluationListener listener) {
370         mEvaluationListener = listener;
371         mEvaluationRequest = request;
372     }
373 
374     // From Evaluator.CharMetricsInfo.
375     @Override
separatorChars(String s, int len)376     public float separatorChars(String s, int len) {
377         int start = 0;
378         while (start < len && !Character.isDigit(s.charAt(start))) {
379             ++start;
380         }
381         // We assume the rest consists of digits, and for consistency with the rest
382         // of the code, we assume all digits have width mCharWidth.
383         final int nDigits = len - start;
384         // We currently insert a digit separator every three digits.
385         final int nSeparators = (nDigits - 1) / 3;
386         synchronized(mWidthLock) {
387             // Always return an upper bound, even in the presence of rounding errors.
388             return nSeparators * mGroupingSeparatorWidthRatio;
389         }
390     }
391 
392     // From Evaluator.CharMetricsInfo.
393     @Override
getNoEllipsisCredit()394     public float getNoEllipsisCredit() {
395         synchronized(mWidthLock) {
396             return mNoEllipsisCredit;
397         }
398     }
399 
400     // From Evaluator.CharMetricsInfo.
401     @Override
getDecimalCredit()402     public float getDecimalCredit() {
403         synchronized(mWidthLock) {
404             return mDecimalCredit;
405         }
406     }
407 
408     // Return the length of the exponent representation for the given exponent, in
409     // characters.
expLen(int exp)410     private final int expLen(int exp) {
411         if (exp == 0) return 0;
412         final int abs_exp_digits = (int) Math.ceil(Math.log10(Math.abs((double)exp))
413                 + 0.0000000001d /* Round whole numbers to next integer */);
414         return abs_exp_digits + (exp >= 0 ? 1 : 2);
415     }
416 
417     /**
418      * Initiate display of a new result.
419      * Only called from UI thread.
420      * The parameters specify various properties of the result.
421      * @param index Index of expression that was just evaluated. Currently ignored, since we only
422      *            expect notification for the expression result being displayed.
423      * @param initPrec Initial display precision computed by evaluator. (1 = tenths digit)
424      * @param msd Position of most significant digit.  Offset from left of string.
425                   Evaluator.INVALID_MSD if unknown.
426      * @param leastDigPos Position of least significant digit (1 = tenths digit)
427      *                    or Integer.MAX_VALUE.
428      * @param truncatedWholePart Result up to but not including decimal point.
429                                  Currently we only use the length.
430      */
431     @Override
onEvaluate(long index, int initPrec, int msd, int leastDigPos, String truncatedWholePart)432     public void onEvaluate(long index, int initPrec, int msd, int leastDigPos,
433             String truncatedWholePart) {
434         initPositions(initPrec, msd, leastDigPos, truncatedWholePart);
435 
436         if (mStoreToMemoryRequested) {
437             mEvaluator.copyToMemory(index);
438             mStoreToMemoryRequested = false;
439         }
440         redisplay();
441     }
442 
443     /**
444      * Store the result for this index if it is available.
445      * If it is unavailable, set mStoreToMemoryRequested to indicate that we should store
446      * when evaluation is complete.
447      */
onMemoryStore()448     public void onMemoryStore() {
449         if (mEvaluator.hasResult(mIndex)) {
450             mEvaluator.copyToMemory(mIndex);
451         } else {
452             mStoreToMemoryRequested = true;
453             mEvaluator.requireResult(mIndex, this /* listener */, this /* CharMetricsInfo */);
454         }
455     }
456 
457     /**
458      * Add the result to the value currently in memory.
459      */
onMemoryAdd()460     public void onMemoryAdd() {
461         mEvaluator.addToMemory(mIndex);
462     }
463 
464     /**
465      * Subtract the result from the value currently in memory.
466      */
onMemorySubtract()467     public void onMemorySubtract() {
468         mEvaluator.subtractFromMemory(mIndex);
469     }
470 
471     /**
472      * Set up scroll bounds (mMinPos, mMaxPos, etc.) and determine whether the result is
473      * scrollable, based on the supplied information about the result.
474      * This is unfortunately complicated because we need to predict whether trailing digits
475      * will eventually be replaced by an exponent.
476      * Just appending the exponent during formatting would be simpler, but would produce
477      * jumpier results during transitions.
478      * Only called from UI thread.
479      */
initPositions(int initPrecOffset, int msdIndex, int lsdOffset, String truncatedWholePart)480     private void initPositions(int initPrecOffset, int msdIndex, int lsdOffset,
481             String truncatedWholePart) {
482         int maxChars = getMaxChars();
483         mWholeLen = truncatedWholePart.length();
484         // Allow a tiny amount of slop for associativity/rounding differences in length
485         // calculation.  If getPreferredPrec() decided it should fit, we want to make it fit, too.
486         // We reserved one extra pixel, so the extra length is OK.
487         final int nSeparatorChars = (int) Math.ceil(
488                 separatorChars(truncatedWholePart, truncatedWholePart.length())
489                 - getNoEllipsisCredit() - 0.0001f);
490         mWholePartFits = mWholeLen + nSeparatorChars <= maxChars;
491         mLastPos = INVALID;
492         mLsdOffset = lsdOffset;
493         mAppendExponent = false;
494         // Prevent scrolling past initial position, which is calculated to show leading digits.
495         mCurrentPos = mMinPos = (int) Math.round(initPrecOffset * mCharWidth);
496         if (msdIndex == Evaluator.INVALID_MSD) {
497             // Possible zero value
498             if (lsdOffset == Integer.MIN_VALUE) {
499                 // Definite zero value.
500                 mMaxPos = mMinPos;
501                 mMaxCharOffset = (int) Math.round(mMaxPos/mCharWidth);
502                 mScrollable = false;
503             } else {
504                 // May be very small nonzero value.  Allow user to find out.
505                 mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL;
506                 mMinPos -= mCharWidth;  // Allow for future minus sign.
507                 mScrollable = true;
508             }
509             return;
510         }
511         int negative = truncatedWholePart.charAt(0) == '-' ? 1 : 0;
512         if (msdIndex > mWholeLen && msdIndex <= mWholeLen + 3) {
513             // Avoid tiny negative exponent; pretend msdIndex is just to the right of decimal point.
514             msdIndex = mWholeLen - 1;
515         }
516         // Set to position of leftmost significant digit relative to dec. point. Usually negative.
517         int minCharOffset = msdIndex - mWholeLen;
518         if (minCharOffset > -1 && minCharOffset < MAX_LEADING_ZEROES + 2) {
519             // Small number of leading zeroes, avoid scientific notation.
520             minCharOffset = -1;
521         }
522         if (lsdOffset < MAX_RIGHT_SCROLL) {
523             mMaxCharOffset = lsdOffset;
524             if (mMaxCharOffset < -1 && mMaxCharOffset > -(MAX_TRAILING_ZEROES + 2)) {
525                 mMaxCharOffset = -1;
526             }
527             // lsdOffset is positive or negative, never 0.
528             int currentExpLen = 0;  // Length of required standard scientific notation exponent.
529             if (mMaxCharOffset < -1) {
530                 currentExpLen = expLen(-minCharOffset - 1);
531             } else if (minCharOffset > -1 || mMaxCharOffset >= maxChars) {
532                 // Number is either entirely to the right of decimal point, or decimal point is
533                 // not visible when scrolled to the right.
534                 currentExpLen = expLen(-minCharOffset);
535             }
536             // Exponent length does not included added decimal point.  But whenever we add a
537             // decimal point, we allow an extra character (SCI_NOTATION_EXTRA).
538             final int separatorLength = mWholePartFits && minCharOffset < -3 ? nSeparatorChars : 0;
539             mScrollable = (mMaxCharOffset + currentExpLen + separatorLength - minCharOffset
540                     + negative >= maxChars);
541             // Now adjust mMaxCharOffset for any required exponent.
542             int newMaxCharOffset;
543             if (currentExpLen > 0) {
544                 if (mScrollable) {
545                     // We'll use exponent corresponding to leastDigPos when scrolled to right.
546                     newMaxCharOffset = mMaxCharOffset + expLen(-lsdOffset);
547                 } else {
548                     newMaxCharOffset = mMaxCharOffset + currentExpLen;
549                 }
550                 if (mMaxCharOffset <= -1 && newMaxCharOffset > -1) {
551                     // Very unlikely; just drop exponent.
552                     mMaxCharOffset = -1;
553                 } else {
554                     mMaxCharOffset = Math.min(newMaxCharOffset, MAX_RIGHT_SCROLL);
555                 }
556                 mMaxPos = Math.min((int) Math.round(mMaxCharOffset * mCharWidth),
557                         MAX_RIGHT_SCROLL);
558             } else if (!mWholePartFits && !mScrollable) {
559                 // Corner case in which entire number fits, but not with grouping separators.  We
560                 // will use an exponent in un-scrolled position, which may hide digits.  Scrolling
561                 // by one character will remove the exponent and reveal the last digits.  Note
562                 // that in the forced scientific notation case, the exponent length is not
563                 // factored into mMaxCharOffset, since we do not want such an increase to impact
564                 // scrolling behavior.  In the unscrollable case, we thus have to append the
565                 // exponent at the end using the forcePrecision argument to formatResult, in order
566                 // to ensure that we get the entire result.
567                 mScrollable = (mMaxCharOffset + expLen(-minCharOffset - 1) - minCharOffset
568                         + negative >= maxChars);
569                 if (mScrollable) {
570                     mMaxPos = (int) Math.ceil(mMinPos + mCharWidth);
571                     // Single character scroll will remove exponent and show remaining piece.
572                 } else {
573                     mMaxPos = mMinPos;
574                     mAppendExponent = true;
575                 }
576             } else {
577                 mMaxPos = Math.min((int) Math.round(mMaxCharOffset * mCharWidth),
578                         MAX_RIGHT_SCROLL);
579             }
580             if (!mScrollable) {
581                 // Position the number consistently with our assumptions to make sure it
582                 // actually fits.
583                 mCurrentPos = mMaxPos;
584             }
585         } else {
586             mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL;
587             mScrollable = true;
588         }
589     }
590 
591     /**
592      * Display error message indicated by resourceId.
593      * UI thread only.
594      */
595     @Override
onError(long index, int resourceId)596     public void onError(long index, int resourceId) {
597         mStoreToMemoryRequested = false;
598         mValid = false;
599         setLongClickable(false);
600         mScrollable = false;
601         final String msg = getContext().getString(resourceId);
602         final float measuredWidth = Layout.getDesiredWidth(msg, getPaint());
603         if (measuredWidth > mWidthConstraint) {
604             // Multiply by .99 to avoid rounding effects.
605             final float scaleFactor = 0.99f * mWidthConstraint / measuredWidth;
606             final RelativeSizeSpan smallTextSpan = new RelativeSizeSpan(scaleFactor);
607             final SpannableString scaledMsg = new SpannableString(msg);
608             scaledMsg.setSpan(smallTextSpan, 0, msg.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
609             setText(scaledMsg);
610         } else {
611             setText(msg);
612         }
613     }
614 
615     private final int MAX_COPY_SIZE = 1000000;
616 
617     /*
618      * Return the most significant digit position in the given string or Evaluator.INVALID_MSD.
619      * Unlike Evaluator.getMsdIndexOf, we treat a final 1 as significant.
620      * Pure function; callable from anywhere.
621      */
getNaiveMsdIndexOf(String s)622     public static int getNaiveMsdIndexOf(String s) {
623         final int len = s.length();
624         for (int i = 0; i < len; ++i) {
625             char c = s.charAt(i);
626             if (c != '-' && c != '.' && c != '0') {
627                 return i;
628             }
629         }
630         return Evaluator.INVALID_MSD;
631     }
632 
633     /**
634      * Format a result returned by Evaluator.getString() into a single line containing ellipses
635      * (if appropriate) and an exponent (if appropriate).
636      * We add two distinct kinds of exponents:
637      * (1) If the final result contains the leading digit we use standard scientific notation.
638      * (2) If not, we add an exponent corresponding to an interpretation of the final result as
639      *     an integer.
640      * We add an ellipsis on the left if the result was truncated.
641      * We add ellipses and exponents in a way that leaves most digits in the position they
642      * would have been in had we not done so. This minimizes jumps as a result of scrolling.
643      * Result is NOT internationalized, uses "E" for exponent.
644      * Called only from UI thread; We sometimes omit locking for fields.
645      * @param precOffset The value that was passed to getString. Identifies the significance of
646                 the rightmost digit. A value of 1 means the rightmost digits corresponds to tenths.
647      * @param maxDigs The maximum number of characters in the result
648      * @param truncated The in parameter was already truncated, beyond possibly removing the
649                 minus sign.
650      * @param negative The in parameter represents a negative result. (Minus sign may be removed
651                 without setting truncated.)
652      * @param lastDisplayedOffset  If not null, we set lastDisplayedOffset[0] to the offset of
653                 the last digit actually appearing in the display.
654      * @param forcePrecision If true, we make sure that the last displayed digit corresponds to
655                 precOffset, and allow maxDigs to be exceeded in adding the exponent and commas.
656      * @param forceSciNotation Force scientific notation. May be set because we don't have
657                 space for grouping separators, but whole number otherwise fits.
658      * @param insertCommas Insert commas (literally, not internationalized) as digit separators.
659                 We only ever do this for the integral part of a number, and only when no
660                 exponent is displayed in the initial position. The combination of which means
661                 that we only do it when no exponent is displayed.
662                 We insert commas in a way that does consider the width of the actual localized digit
663                 separator. Commas count towards maxDigs as the appropriate fraction of a digit.
664      */
formatResult(String in, int precOffset, int maxDigs, boolean truncated, boolean negative, int lastDisplayedOffset[], boolean forcePrecision, boolean forceSciNotation, boolean insertCommas)665     private String formatResult(String in, int precOffset, int maxDigs, boolean truncated,
666             boolean negative, int lastDisplayedOffset[], boolean forcePrecision,
667             boolean forceSciNotation, boolean insertCommas) {
668         final int minusSpace = negative ? 1 : 0;
669         final int msdIndex = truncated ? -1 : getNaiveMsdIndexOf(in);  // INVALID_MSD is OK.
670         String result = in;
671         boolean needEllipsis = false;
672         if (truncated || (negative && result.charAt(0) != '-')) {
673             needEllipsis = true;
674             result = KeyMaps.ELLIPSIS + result.substring(1, result.length());
675             // Ellipsis may be removed again in the type(1) scientific notation case.
676         }
677         final int decIndex = result.indexOf('.');
678         if (lastDisplayedOffset != null) {
679             lastDisplayedOffset[0] = precOffset;
680         }
681         if (forceSciNotation || (decIndex == -1 || msdIndex != Evaluator.INVALID_MSD
682                 && msdIndex - decIndex > MAX_LEADING_ZEROES + 1) &&  precOffset != -1) {
683             // Either:
684             // 1) No decimal point displayed, and it's not just to the right of the last digit, or
685             // 2) we are at the front of a number whos integral part is too large to allow
686             // comma insertion, or
687             // 3) we should suppress leading zeroes.
688             // Add an exponent to let the user track which digits are currently displayed.
689             // Start with type (2) exponent if we dropped no digits. -1 accounts for decimal point.
690             // We currently never show digit separators together with an exponent.
691             final int initExponent = precOffset > 0 ? -precOffset : -precOffset - 1;
692             int exponent = initExponent;
693             boolean hasPoint = false;
694             if (!truncated && msdIndex < maxDigs - 1
695                     && result.length() - msdIndex + 1 + minusSpace
696                     <= maxDigs + SCI_NOTATION_EXTRA) {
697                 // Type (1) exponent computation and transformation:
698                 // Leading digit is in display window. Use standard calculator scientific notation
699                 // with one digit to the left of the decimal point. Insert decimal point and
700                 // delete leading zeroes.
701                 // We try to keep leading digits roughly in position, and never
702                 // lengthen the result by more than SCI_NOTATION_EXTRA.
703                 if (decIndex > msdIndex) {
704                     // In the forceSciNotation, we can have a decimal point in the relevant digit
705                     // range. Remove it.
706                     result = result.substring(0, decIndex)
707                             + result.substring(decIndex + 1, result.length());
708                     // msdIndex and precOffset unaffected.
709                 }
710                 final int resLen = result.length();
711                 String fraction = result.substring(msdIndex + 1, resLen);
712                 result = (negative ? "-" : "") + result.substring(msdIndex, msdIndex + 1)
713                         + "." + fraction;
714                 // Original exp was correct for decimal point at right of fraction.
715                 // Adjust by length of fraction.
716                 exponent = initExponent + resLen - msdIndex - 1;
717                 hasPoint = true;
718             }
719             // Exponent can't be zero.
720             // Actually add the exponent of either type:
721             if (!forcePrecision) {
722                 int dropDigits;  // Digits to drop to make room for exponent.
723                 if (hasPoint) {
724                     // Type (1) exponent.
725                     // Drop digits even if there is room. Otherwise the scrolling gets jumpy.
726                     dropDigits = expLen(exponent);
727                     if (dropDigits >= result.length() - 1) {
728                         // Jumpy is better than no mantissa.  Probably impossible anyway.
729                         dropDigits = Math.max(result.length() - 2, 0);
730                     }
731                 } else {
732                     // Type (2) exponent.
733                     // Exponent depends on the number of digits we drop, which depends on
734                     // exponent ...
735                     for (dropDigits = 2; expLen(initExponent + dropDigits) > dropDigits;
736                             ++dropDigits) {}
737                     exponent = initExponent + dropDigits;
738                     if (precOffset - dropDigits > mLsdOffset) {
739                         // This can happen if e.g. result = 10^40 + 10^10
740                         // It turns out we would otherwise display ...10e9 because it takes
741                         // the same amount of space as ...1e10 but shows one more digit.
742                         // But we don't want to display a trailing zero, even if it's free.
743                         ++dropDigits;
744                         ++exponent;
745                     }
746                 }
747                 if (dropDigits >= result.length() - 1) {
748                     // Display too small to show meaningful result.
749                     return KeyMaps.ELLIPSIS + "E" + KeyMaps.ELLIPSIS;
750                 }
751                 result = result.substring(0, result.length() - dropDigits);
752                 if (lastDisplayedOffset != null) {
753                     lastDisplayedOffset[0] -= dropDigits;
754                 }
755             }
756             result = result + "E" + Integer.toString(exponent);
757         } else if (insertCommas) {
758             // Add commas to the whole number section, and then truncate on left to fit,
759             // counting commas as a fractional digit.
760             final int wholeStart = needEllipsis ? 1 : 0;
761             int orig_length = result.length();
762             final float nCommaChars;
763             if (decIndex != -1) {
764                 nCommaChars = separatorChars(result, decIndex);
765                 result = StringUtils.addCommas(result, wholeStart, decIndex)
766                         + result.substring(decIndex, orig_length);
767             } else {
768                 nCommaChars = separatorChars(result, orig_length);
769                 result = StringUtils.addCommas(result, wholeStart, orig_length);
770             }
771             if (needEllipsis) {
772                 orig_length -= 1;  // Exclude ellipsis.
773             }
774             final float len = orig_length + nCommaChars;
775             int deletedChars = 0;
776             final float ellipsisCredit = getNoEllipsisCredit();
777             final float decimalCredit = getDecimalCredit();
778             final float effectiveLen = len - (decIndex == -1 ? 0 : getDecimalCredit());
779             final float ellipsisAdjustment =
780                     needEllipsis ? mNoExponentCredit : getNoEllipsisCredit();
781             // As above, we allow for a tiny amount of extra length here, for consistency with
782             // getPreferredPrec().
783             if (effectiveLen - ellipsisAdjustment > (float) (maxDigs - wholeStart) + 0.0001f
784                 && !forcePrecision) {
785                 float deletedWidth = 0.0f;
786                 while (effectiveLen - mNoExponentCredit - deletedWidth
787                         > (float) (maxDigs - 1 /* for ellipsis */)) {
788                     if (result.charAt(deletedChars) == ',') {
789                         deletedWidth += mGroupingSeparatorWidthRatio;
790                     } else {
791                         deletedWidth += 1.0f;
792                     }
793                     deletedChars++;
794                 }
795             }
796             if (deletedChars > 0) {
797                 result = KeyMaps.ELLIPSIS + result.substring(deletedChars, result.length());
798             } else if (needEllipsis) {
799                 result = KeyMaps.ELLIPSIS + result;
800             }
801         }
802         return result;
803     }
804 
805     /**
806      * Get formatted, but not internationalized, result from mEvaluator.
807      * @param precOffset requested position (1 = tenths) of last included digit
808      * @param maxSize maximum number of characters (more or less) in result
809      * @param lastDisplayedOffset zeroth entry is set to actual offset of last included digit,
810      *                            after adjusting for exponent, etc.  May be null.
811      * @param forcePrecision Ensure that last included digit is at pos, at the expense
812      *                       of treating maxSize as a soft limit.
813      * @param forceSciNotation Force scientific notation, even if not required by maxSize.
814      * @param insertCommas Insert commas as digit separators.
815      */
getFormattedResult(int precOffset, int maxSize, int lastDisplayedOffset[], boolean forcePrecision, boolean forceSciNotation, boolean insertCommas)816     private String getFormattedResult(int precOffset, int maxSize, int lastDisplayedOffset[],
817             boolean forcePrecision, boolean forceSciNotation, boolean insertCommas) {
818         final boolean truncated[] = new boolean[1];
819         final boolean negative[] = new boolean[1];
820         final int requestedPrecOffset[] = {precOffset};
821         final String rawResult = mEvaluator.getString(mIndex, requestedPrecOffset, mMaxCharOffset,
822                 maxSize, truncated, negative, this);
823         return formatResult(rawResult, requestedPrecOffset[0], maxSize, truncated[0], negative[0],
824                 lastDisplayedOffset, forcePrecision, forceSciNotation, insertCommas);
825    }
826 
827     /**
828      * Return entire result (within reason) up to current displayed precision.
829      * @param withSeparators  Add digit separators
830      */
getFullText(boolean withSeparators)831     public String getFullText(boolean withSeparators) {
832         if (!mValid) return "";
833         if (!mScrollable) return getText().toString();
834         return KeyMaps.translateResult(getFormattedResult(mLastDisplayedOffset, MAX_COPY_SIZE,
835                 null, true /* forcePrecision */, false /* forceSciNotation */, withSeparators));
836     }
837 
838     /**
839      * Did the above produce a correct result?
840      * UI thread only.
841      */
fullTextIsExact()842     public boolean fullTextIsExact() {
843         return !mScrollable || (getCharOffset(mMaxPos) == getCharOffset(mCurrentPos)
844                 && mMaxCharOffset != MAX_RIGHT_SCROLL);
845     }
846 
847     /**
848      * Get entire result up to current displayed precision, or up to MAX_COPY_EXTRA additional
849      * digits, if it will lead to an exact result.
850      */
getFullCopyText()851     public String getFullCopyText() {
852         if (!mValid
853                 || mLsdOffset == Integer.MAX_VALUE
854                 || fullTextIsExact()
855                 || mWholeLen > MAX_RECOMPUTE_DIGITS
856                 || mWholeLen + mLsdOffset > MAX_RECOMPUTE_DIGITS
857                 || mLsdOffset - mLastDisplayedOffset > MAX_COPY_EXTRA) {
858             return getFullText(false /* withSeparators */);
859         }
860         // It's reasonable to compute and copy the exact result instead.
861         int fractionLsdOffset = Math.max(0, mLsdOffset);
862         String rawResult = mEvaluator.getResult(mIndex).toStringTruncated(fractionLsdOffset);
863         if (mLsdOffset <= -1) {
864             // Result has trailing decimal point. Remove it.
865             rawResult = rawResult.substring(0, rawResult.length() - 1);
866             fractionLsdOffset = -1;
867         }
868         final String formattedResult = formatResult(rawResult, fractionLsdOffset, MAX_COPY_SIZE,
869                 false, rawResult.charAt(0) == '-', null, true /* forcePrecision */,
870                 false /* forceSciNotation */, false /* insertCommas */);
871         return KeyMaps.translateResult(formattedResult);
872     }
873 
874     /**
875      * Return the maximum number of characters that will fit in the result display.
876      * May be called asynchronously from non-UI thread. From Evaluator.CharMetricsInfo.
877      * Returns zero if measurement hasn't completed.
878      */
879     @Override
getMaxChars()880     public int getMaxChars() {
881         int result;
882         synchronized(mWidthLock) {
883             return (int) Math.floor(mWidthConstraint / mCharWidth);
884         }
885     }
886 
887     /**
888      * @return {@code true} if the currently displayed result is scrollable
889      */
isScrollable()890     public boolean isScrollable() {
891         return mScrollable;
892     }
893 
894     /**
895      * Map pixel position to digit offset.
896      * UI thread only.
897      */
getCharOffset(int pos)898     int getCharOffset(int pos) {
899         return (int) Math.round(pos / mCharWidth);  // Lock not needed.
900     }
901 
clear()902     void clear() {
903         mValid = false;
904         mScrollable = false;
905         setText("");
906         setLongClickable(false);
907     }
908 
909     @Override
onCancelled(long index)910     public void onCancelled(long index) {
911         clear();
912         mStoreToMemoryRequested = false;
913     }
914 
915     /**
916      * Refresh display.
917      * Only called in UI thread. Index argument is currently ignored.
918      */
919     @Override
onReevaluate(long index)920     public void onReevaluate(long index) {
921         redisplay();
922     }
923 
redisplay()924     public void redisplay() {
925         int maxChars = getMaxChars();
926         if (maxChars < 4) {
927             // Display currently too small to display a reasonable result. Punt to avoid crash.
928             return;
929         }
930         if (mScroller.isFinished() && length() > 0) {
931             setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
932         }
933         int currentCharOffset = getCharOffset(mCurrentPos);
934         int lastDisplayedOffset[] = new int[1];
935         String result = getFormattedResult(currentCharOffset, maxChars, lastDisplayedOffset,
936                 mAppendExponent /* forcePrecision; preserve entire result */,
937                 !mWholePartFits
938                 &&  currentCharOffset == getCharOffset(mMinPos) /* forceSciNotation */,
939                 mWholePartFits /* insertCommas */ );
940         int expIndex = result.indexOf('E');
941         result = KeyMaps.translateResult(result);
942         if (expIndex > 0 && result.indexOf('.') == -1) {
943           // Gray out exponent if used as position indicator
944             SpannableString formattedResult = new SpannableString(result);
945             formattedResult.setSpan(mExponentColorSpan, expIndex, result.length(),
946                                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
947             setText(formattedResult);
948         } else {
949             setText(result);
950         }
951         mLastDisplayedOffset = lastDisplayedOffset[0];
952         mValid = true;
953         setLongClickable(true);
954     }
955 
956     @Override
onTextChanged(java.lang.CharSequence text, int start, int lengthBefore, int lengthAfter)957     protected void onTextChanged(java.lang.CharSequence text, int start, int lengthBefore,
958             int lengthAfter) {
959         super.onTextChanged(text, start, lengthBefore, lengthAfter);
960 
961         if (!mScrollable || mScroller.isFinished()) {
962             if (lengthBefore == 0 && lengthAfter > 0) {
963                 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
964                 setContentDescription(null);
965             } else if (lengthBefore > 0 && lengthAfter == 0) {
966                 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_NONE);
967                 setContentDescription(getContext().getString(R.string.desc_result));
968             }
969         }
970     }
971 
972     @Override
computeScroll()973     public void computeScroll() {
974         if (!mScrollable) {
975             return;
976         }
977 
978         if (mScroller.computeScrollOffset()) {
979             mCurrentPos = mScroller.getCurrX();
980             if (getCharOffset(mCurrentPos) != getCharOffset(mLastPos)) {
981                 mLastPos = mCurrentPos;
982                 redisplay();
983             }
984         }
985 
986         if (!mScroller.isFinished()) {
987                 postInvalidateOnAnimation();
988                 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_NONE);
989         } else if (length() > 0){
990             setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
991         }
992     }
993 
994     /**
995      * Use ActionMode for copy/memory support on M and higher.
996      */
997     @TargetApi(Build.VERSION_CODES.M)
setupActionMode()998     private void setupActionMode() {
999         mCopyActionModeCallback = new ActionMode.Callback2() {
1000 
1001             @Override
1002             public boolean onCreateActionMode(ActionMode mode, Menu menu) {
1003                 final MenuInflater inflater = mode.getMenuInflater();
1004                 return createContextMenu(inflater, menu);
1005             }
1006 
1007             @Override
1008             public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
1009                 return false; // Return false if nothing is done
1010             }
1011 
1012             @Override
1013             public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
1014                 if (onMenuItemClick(item)) {
1015                     mode.finish();
1016                     return true;
1017                 } else {
1018                     return false;
1019                 }
1020             }
1021 
1022             @Override
1023             public void onDestroyActionMode(ActionMode mode) {
1024                 unhighlightResult();
1025                 mActionMode = null;
1026             }
1027 
1028             @Override
1029             public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
1030                 super.onGetContentRect(mode, view, outRect);
1031 
1032                 outRect.left += view.getPaddingLeft();
1033                 outRect.top += view.getPaddingTop();
1034                 outRect.right -= view.getPaddingRight();
1035                 outRect.bottom -= view.getPaddingBottom();
1036                 final int width = (int) Layout.getDesiredWidth(getText(), getPaint());
1037                 if (width < outRect.width()) {
1038                     outRect.left = outRect.right - width;
1039                 }
1040 
1041                 if (!BuildCompat.isAtLeastN()) {
1042                     // The CAB (prior to N) only takes the translation of a view into account, so
1043                     // if a scale is applied to the view then the offset outRect will end up being
1044                     // positioned incorrectly. We workaround that limitation by manually applying
1045                     // the scale to the outRect, which the CAB will then offset to the correct
1046                     // position.
1047                     final float scaleX = view.getScaleX();
1048                     final float scaleY = view.getScaleY();
1049                     outRect.left *= scaleX;
1050                     outRect.right *= scaleX;
1051                     outRect.top *= scaleY;
1052                     outRect.bottom *= scaleY;
1053                 }
1054             }
1055         };
1056         setOnLongClickListener(new View.OnLongClickListener() {
1057             @Override
1058             public boolean onLongClick(View v) {
1059                 if (mValid) {
1060                     mActionMode = startActionMode(mCopyActionModeCallback,
1061                             ActionMode.TYPE_FLOATING);
1062                     return true;
1063                 }
1064                 return false;
1065             }
1066         });
1067     }
1068 
1069     /**
1070      * Use ContextMenu for copy/memory support on L and lower.
1071      */
setupContextMenu()1072     private void setupContextMenu() {
1073         setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
1074             @Override
1075             public void onCreateContextMenu(ContextMenu contextMenu, View view,
1076                     ContextMenu.ContextMenuInfo contextMenuInfo) {
1077                 final MenuInflater inflater = new MenuInflater(getContext());
1078                 createContextMenu(inflater, contextMenu);
1079                 mContextMenu = contextMenu;
1080                 for (int i = 0; i < contextMenu.size(); i ++) {
1081                     contextMenu.getItem(i).setOnMenuItemClickListener(CalculatorResult.this);
1082                 }
1083             }
1084         });
1085         setOnLongClickListener(new View.OnLongClickListener() {
1086             @Override
1087             public boolean onLongClick(View v) {
1088                 if (mValid) {
1089                     return showContextMenu();
1090                 }
1091                 return false;
1092             }
1093         });
1094     }
1095 
createContextMenu(MenuInflater inflater, Menu menu)1096     private boolean createContextMenu(MenuInflater inflater, Menu menu) {
1097         inflater.inflate(R.menu.menu_result, menu);
1098         final boolean displayMemory = mEvaluator.getMemoryIndex() != 0;
1099         final MenuItem memoryAddItem = menu.findItem(R.id.memory_add);
1100         final MenuItem memorySubtractItem = menu.findItem(R.id.memory_subtract);
1101         memoryAddItem.setEnabled(displayMemory);
1102         memorySubtractItem.setEnabled(displayMemory);
1103         highlightResult();
1104         return true;
1105     }
1106 
stopActionModeOrContextMenu()1107     public boolean stopActionModeOrContextMenu() {
1108         if (mActionMode != null) {
1109             mActionMode.finish();
1110             return true;
1111         }
1112         if (mContextMenu != null) {
1113             unhighlightResult();
1114             mContextMenu.close();
1115             return true;
1116         }
1117         return false;
1118     }
1119 
highlightResult()1120     private void highlightResult() {
1121         final Spannable text = (Spannable) getText();
1122         text.setSpan(mHighlightSpan, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1123     }
1124 
unhighlightResult()1125     private void unhighlightResult() {
1126         final Spannable text = (Spannable) getText();
1127         text.removeSpan(mHighlightSpan);
1128     }
1129 
setPrimaryClip(ClipData clip)1130     private void setPrimaryClip(ClipData clip) {
1131         ClipboardManager clipboard = (ClipboardManager) getContext().
1132                                                getSystemService(Context.CLIPBOARD_SERVICE);
1133         clipboard.setPrimaryClip(clip);
1134     }
1135 
copyContent()1136     private void copyContent() {
1137         final CharSequence text = getFullCopyText();
1138         ClipboardManager clipboard =
1139                 (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
1140         // We include a tag URI, to allow us to recognize our own results and handle them
1141         // specially.
1142         ClipData.Item newItem = new ClipData.Item(text, null, mEvaluator.capture(mIndex));
1143         String[] mimeTypes = new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN};
1144         ClipData cd = new ClipData("calculator result", mimeTypes, newItem);
1145         clipboard.setPrimaryClip(cd);
1146         Toast.makeText(getContext(), R.string.text_copied_toast, Toast.LENGTH_SHORT).show();
1147     }
1148 
1149     @Override
onMenuItemClick(MenuItem item)1150     public boolean onMenuItemClick(MenuItem item) {
1151         switch (item.getItemId()) {
1152             case R.id.memory_add:
1153                 onMemoryAdd();
1154                 return true;
1155             case R.id.memory_subtract:
1156                 onMemorySubtract();
1157                 return true;
1158             case R.id.memory_store:
1159                 onMemoryStore();
1160                 return true;
1161             case R.id.menu_copy:
1162                 if (mEvaluator.evaluationInProgress(mIndex)) {
1163                     // Refuse to copy placeholder characters.
1164                     return false;
1165                 } else {
1166                     copyContent();
1167                     unhighlightResult();
1168                     return true;
1169                 }
1170             default:
1171                 return false;
1172         }
1173     }
1174 
1175     @Override
onDetachedFromWindow()1176     protected void onDetachedFromWindow() {
1177         stopActionModeOrContextMenu();
1178         super.onDetachedFromWindow();
1179     }
1180 }
1181