summaryrefslogtreecommitdiff
path: root/src/com/android/calculator2/CalculatorResult.java
blob: 8fa698b94bf55eca2869f26a2c1c5e8598379cb9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.calculator2;

import android.annotation.TargetApi;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.ClipboardManager;
import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
import androidx.annotation.IntDef;
import androidx.core.content.ContextCompat;
import androidx.core.os.BuildCompat;
import android.text.Layout;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan;
import android.util.AttributeSet;
import android.view.ActionMode;
import android.view.ContextMenu;
import android.view.GestureDetector;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.OverScroller;
import android.widget.Toast;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

// A text widget that is "infinitely" scrollable to the right,
// and obtains the text to display via a callback to Logic.
public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenuItemClickListener,
        Evaluator.EvaluationListener, Evaluator.CharMetricsInfo {
    static final int MAX_RIGHT_SCROLL = 10000000;
    static final int INVALID = MAX_RIGHT_SCROLL + 10000;
        // A larger value is unlikely to avoid running out of space
    final OverScroller mScroller;
    final GestureDetector mGestureDetector;
    private long mIndex;  // Index of expression we are displaying.
    private Evaluator mEvaluator;
    private boolean mScrollable = false;
                            // A scrollable result is currently displayed.
    private boolean mValid = false;
                            // The result holds a valid number (not an error message).
    // A suffix of "Pos" denotes a pixel offset.  Zero represents a scroll position
    // in which the decimal point is just barely visible on the right of the display.
    private int mCurrentPos;// Position of right of display relative to decimal point, in pixels.
                            // Large positive values mean the decimal point is scrolled off the
                            // left of the display.  Zero means decimal point is barely displayed
                            // on the right.
    private int mLastPos;   // Position already reflected in display. Pixels.
    private int mMinPos;    // Minimum position to avoid unnecessary blanks on the left. Pixels.
    private int mMaxPos;    // Maximum position before we start displaying the infinite
                            // sequence of trailing zeroes on the right. Pixels.
    private int mWholeLen;  // Length of the whole part of current result.
    // In the following, we use a suffix of Offset to denote a character position in a numeric
    // string relative to the decimal point.  Positive is to the right and negative is to
    // the left. 1 = tenths position, -1 = units.  Integer.MAX_VALUE is sometimes used
    // for the offset of the last digit in an a nonterminating decimal expansion.
    // We use the suffix "Index" to denote a zero-based index into a string representing a
    // result.
    private int mMaxCharOffset;  // Character offset from decimal point of rightmost digit
                                 // that should be displayed, plus the length of any exponent
                                 // needed to display that digit.
                                 // Limited to MAX_RIGHT_SCROLL. Often the same as:
    private int mLsdOffset;      // Position of least-significant digit in result
    private int mLastDisplayedOffset; // Offset of last digit actually displayed after adding
                                      // exponent.
    private boolean mWholePartFits;  // Scientific notation not needed for initial display.
    private float mNoExponentCredit;
                            // Fraction of digit width saved by avoiding scientific notation.
                            // Only accessed from UI thread.
    private boolean mAppendExponent;
                            // The result fits entirely in the display, even with an exponent,
                            // but not with grouping separators. Since the result is not
                            // scrollable, and we do not add the exponent to max. scroll position,
                            // append an exponent insteadd of replacing trailing digits.
    private final Object mWidthLock = new Object();
                            // Protects the next five fields.  These fields are only
                            // updated by the UI thread, and read accesses by the UI thread
                            // sometimes do not acquire the lock.
    private int mWidthConstraint = 0;
                            // Our total width in pixels minus space for ellipsis.
                            // 0 ==> uninitialized.
    private float mCharWidth = 1;
                            // Maximum character width. For now we pretend that all characters
                            // have this width.
                            // TODO: We're not really using a fixed width font.  But it appears
                            // to be close enough for the characters we use that the difference
                            // is not noticeable.
    private float mGroupingSeparatorWidthRatio;
                            // Fraction of digit width occupied by a digit separator.
    private float mDecimalCredit;
                            // Fraction of digit width saved by replacing digit with decimal point.
    private float mNoEllipsisCredit;
                            // Fraction of digit width saved by both replacing ellipsis with digit
                            // and avoiding scientific notation.
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({SHOULD_REQUIRE, SHOULD_EVALUATE, SHOULD_NOT_EVALUATE})
    public @interface EvaluationRequest {}
    public static final int SHOULD_REQUIRE = 2;
    public static final int SHOULD_EVALUATE = 1;
    public static final int SHOULD_NOT_EVALUATE = 0;
    @EvaluationRequest private int mEvaluationRequest = SHOULD_REQUIRE;
                            // Should we evaluate when layout completes, and how?
    private Evaluator.EvaluationListener mEvaluationListener = this;
                            // Listener to use if/when evaluation is requested.
    public static final int MAX_LEADING_ZEROES = 6;
                            // Maximum number of leading zeroes after decimal point before we
                            // switch to scientific notation with negative exponent.
    public static final int MAX_TRAILING_ZEROES = 6;
                            // Maximum number of trailing zeroes before the decimal point before
                            // we switch to scientific notation with positive exponent.
    private static final int SCI_NOTATION_EXTRA = 1;
                            // Extra digits for standard scientific notation.  In this case we
                            // have a decimal point and no ellipsis.
                            // We assume that we do not drop digits to make room for the decimal
                            // point in ordinary scientific notation. Thus >= 1.
    private static final int MAX_COPY_EXTRA = 100;
                            // The number of extra digits we are willing to compute to copy
                            // a result as an exact number.
    private static final int MAX_RECOMPUTE_DIGITS = 2000;
                            // The maximum number of digits we're willing to recompute in the UI
                            // thread.  We only do this for known rational results, where we
                            // can bound the computation cost.
    private final ForegroundColorSpan mExponentColorSpan;
    private final BackgroundColorSpan mHighlightSpan;

    private ActionMode mActionMode;
    private ActionMode.Callback mCopyActionModeCallback;
    private ContextMenu mContextMenu;

    // The user requested that the result currently being evaluated should be stored to "memory".
    private boolean mStoreToMemoryRequested = false;

    public CalculatorResult(Context context, AttributeSet attrs) {
        super(context, attrs);
        mScroller = new OverScroller(context);
        mHighlightSpan = new BackgroundColorSpan(getHighlightColor());
        mExponentColorSpan = new ForegroundColorSpan(
                ContextCompat.getColor(context, R.color.display_result_exponent_text_color));
        mGestureDetector = new GestureDetector(context,
            new GestureDetector.SimpleOnGestureListener() {
                @Override
                public boolean onDown(MotionEvent e) {
                    return true;
                }
                @Override
                public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
                        float velocityY) {
                    if (!mScroller.isFinished()) {
                        mCurrentPos = mScroller.getFinalX();
                    }
                    mScroller.forceFinished(true);
                    stopActionModeOrContextMenu();
                    CalculatorResult.this.cancelLongPress();
                    // Ignore scrolls of error string, etc.
                    if (!mScrollable) return true;
                    mScroller.fling(mCurrentPos, 0, - (int) velocityX, 0  /* horizontal only */,
                                    mMinPos, mMaxPos, 0, 0);
                    postInvalidateOnAnimation();
                    return true;
                }
                @Override
                public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
                        float distanceY) {
                    int distance = (int)distanceX;
                    if (!mScroller.isFinished()) {
                        mCurrentPos = mScroller.getFinalX();
                    }
                    mScroller.forceFinished(true);
                    stopActionModeOrContextMenu();
                    CalculatorResult.this.cancelLongPress();
                    if (!mScrollable) return true;
                    if (mCurrentPos + distance < mMinPos) {
                        distance = mMinPos - mCurrentPos;
                    } else if (mCurrentPos + distance > mMaxPos) {
                        distance = mMaxPos - mCurrentPos;
                    }
                    int duration = (int)(e2.getEventTime() - e1.getEventTime());
                    if (duration < 1 || duration > 100) duration = 10;
                    mScroller.startScroll(mCurrentPos, 0, distance, 0, (int)duration);
                    postInvalidateOnAnimation();
                    return true;
                }
                @Override
                public void onLongPress(MotionEvent e) {
                    if (mValid) {
                        performLongClick();
                    }
                }
            });

        final int slop = ViewConfiguration.get(context).getScaledTouchSlop();
        setOnTouchListener(new View.OnTouchListener() {

            // Used to determine whether a touch event should be intercepted.
            private float mInitialDownX;
            private float mInitialDownY;

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                final int action = event.getActionMasked();

                final float x = event.getX();
                final float y = event.getY();
                switch (action) {
                    case MotionEvent.ACTION_DOWN:
                        mInitialDownX = x;
                        mInitialDownY = y;
                        break;
                    case MotionEvent.ACTION_MOVE:
                        final float deltaX = Math.abs(x - mInitialDownX);
                        final float deltaY = Math.abs(y - mInitialDownY);
                        if (deltaX > slop && deltaX > deltaY) {
                            // Prevent the DragLayout from intercepting horizontal scrolls.
                            getParent().requestDisallowInterceptTouchEvent(true);
                        }
                }
                return mGestureDetector.onTouchEvent(event);
            }
        });

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            setupActionMode();
        } else {
            setupContextMenu();
        }

        setCursorVisible(false);
        setLongClickable(false);
        setContentDescription(context.getString(R.string.desc_result));
    }

    void setEvaluator(Evaluator evaluator, long index) {
        mEvaluator = evaluator;
        mIndex = index;
        requestLayout();
    }

    // Compute maximum digit width the hard way.
    private static float getMaxDigitWidth(TextPaint paint) {
        // Compute the maximum advance width for each digit, thus accounting for between-character
        // spaces. If we ever support other kinds of digits, we may have to avoid kerning effects
        // that could reduce the advance width within this particular string.
        final String allDigits = "0123456789";
        final float[] widths = new float[allDigits.length()];
        paint.getTextWidths(allDigits, widths);
        float maxWidth = 0;
        for (float x : widths) {
            maxWidth = Math.max(x, maxWidth);
        }
        return maxWidth;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (!isLaidOut()) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            // Set a minimum height so scaled error messages won't affect our layout.
            setMinimumHeight(getLineHeight() + getCompoundPaddingBottom()
                    + getCompoundPaddingTop());
        }

        final TextPaint paint = getPaint();
        final Context context = getContext();
        final float newCharWidth = getMaxDigitWidth(paint);
        // Digits are presumed to have no more than newCharWidth.
        // There are two instances when we know that the result is otherwise narrower than
        // expected:
        // 1. For standard scientific notation (our type 1), we know that we have a norrow decimal
        // point and no (usually wide) ellipsis symbol. We allow one extra digit
        // (SCI_NOTATION_EXTRA) to compensate, and consider that in determining available width.
        // 2. If we are using digit grouping separators and a decimal point, we give ourselves
        // a fractional extra space for those separators, the value of which depends on whether
        // there is also an ellipsis.
        //
        // Maximum extra space we need in various cases:
        // Type 1 scientific notation, assuming ellipsis, minus sign and E are wider than a digit:
        //    Two minus signs + "E" + "." - 3 digits.
        // Type 2 scientific notation:
        //    Ellipsis + "E" + "-" - 3 digits.
        // In the absence of scientific notation, we may need a little less space.
        // We give ourselves a bit of extra credit towards comma insertion and give
        // ourselves more if we have either
        //    No ellipsis, or
        //    A decimal separator.

        // Calculate extra space we need to reserve, in addition to character count.
        final float decimalSeparatorWidth = Layout.getDesiredWidth(
                context.getString(R.string.dec_point), paint);
        final float minusWidth = Layout.getDesiredWidth(context.getString(R.string.op_sub), paint);
        final float minusExtraWidth = Math.max(minusWidth - newCharWidth, 0.0f);
        final float ellipsisWidth = Layout.getDesiredWidth(KeyMaps.ELLIPSIS, paint);
        final float ellipsisExtraWidth =  Math.max(ellipsisWidth - newCharWidth, 0.0f);
        final float expWidth = Layout.getDesiredWidth(KeyMaps.translateResult("e"), paint);
        final float expExtraWidth =  Math.max(expWidth - newCharWidth, 0.0f);
        final float type1Extra = 2 * minusExtraWidth + expExtraWidth + decimalSeparatorWidth;
        final float type2Extra = ellipsisExtraWidth + expExtraWidth + minusExtraWidth;
        final float extraWidth = Math.max(type1Extra, type2Extra);
        final int intExtraWidth = (int) Math.ceil(extraWidth) + 1 /* to cover rounding sins */;
        final int newWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
                - (getPaddingLeft() + getPaddingRight()) - intExtraWidth;

        // Calculate other width constants we need to handle grouping separators.
        final float groupingSeparatorW =
                Layout.getDesiredWidth(KeyMaps.translateResult(","), paint);
        // Credits in the absence of any scientific notation:
        float noExponentCredit = extraWidth - Math.max(ellipsisExtraWidth, minusExtraWidth);
        final float noEllipsisCredit = extraWidth - minusExtraWidth;  // includes noExponentCredit.
        final float decimalCredit = Math.max(newCharWidth - decimalSeparatorWidth, 0.0f);

        mNoExponentCredit = noExponentCredit / newCharWidth;
        synchronized(mWidthLock) {
            mWidthConstraint = newWidthConstraint;
            mCharWidth = newCharWidth;
            mNoEllipsisCredit = noEllipsisCredit / newCharWidth;
            mDecimalCredit = decimalCredit / newCharWidth;
            mGroupingSeparatorWidthRatio = groupingSeparatorW / newCharWidth;
        }

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

        if (mEvaluator != null && mEvaluationRequest != SHOULD_NOT_EVALUATE) {
            final CalculatorExpr expr = mEvaluator.getExpr(mIndex);
            if (expr != null && expr.hasInterestingOps()) {
                if (mEvaluationRequest == SHOULD_REQUIRE) {
                    mEvaluator.requireResult(mIndex, mEvaluationListener, this);
                } else {
                    mEvaluator.evaluateAndNotify(mIndex, mEvaluationListener, this);
                }
            }
        }
    }

    /**
     * Specify whether we should evaluate result on layout.
     * @param should one of SHOULD_REQUIRE, SHOULD_EVALUATE, SHOULD_NOT_EVALUATE
     */
    public void setShouldEvaluateResult(@EvaluationRequest int request,
            Evaluator.EvaluationListener listener) {
        mEvaluationListener = listener;
        mEvaluationRequest = request;
    }

    // From Evaluator.CharMetricsInfo.
    @Override
    public float separatorChars(String s, int len) {
        int start = 0;
        while (start < len && !Character.isDigit(s.charAt(start))) {
            ++start;
        }
        // We assume the rest consists of digits, and for consistency with the rest
        // of the code, we assume all digits have width mCharWidth.
        final int nDigits = len - start;
        // We currently insert a digit separator every three digits.
        final int nSeparators = (nDigits - 1) / 3;
        synchronized(mWidthLock) {
            // Always return an upper bound, even in the presence of rounding errors.
            return nSeparators * mGroupingSeparatorWidthRatio;
        }
    }

    // From Evaluator.CharMetricsInfo.
    @Override
    public float getNoEllipsisCredit() {
        synchronized(mWidthLock) {
            return mNoEllipsisCredit;
        }
    }

    // From Evaluator.CharMetricsInfo.
    @Override
    public float getDecimalCredit() {
        synchronized(mWidthLock) {
            return mDecimalCredit;
        }
    }

    // Return the length of the exponent representation for the given exponent, in
    // characters.
    private final int expLen(int exp) {
        if (exp == 0) return 0;
        final int abs_exp_digits = (int) Math.ceil(Math.log10(Math.abs((double)exp))
                + 0.0000000001d /* Round whole numbers to next integer */);
        return abs_exp_digits + (exp >= 0 ? 1 : 2);
    }

    /**
     * Initiate display of a new result.
     * Only called from UI thread.
     * The parameters specify various properties of the result.
     * @param index Index of expression that was just evaluated. Currently ignored, since we only
     *            expect notification for the expression result being displayed.
     * @param initPrec Initial display precision computed by evaluator. (1 = tenths digit)
     * @param msd Position of most significant digit.  Offset from left of string.
                  Evaluator.INVALID_MSD if unknown.
     * @param leastDigPos Position of least significant digit (1 = tenths digit)
     *                    or Integer.MAX_VALUE.
     * @param truncatedWholePart Result up to but not including decimal point.
                                 Currently we only use the length.
     */
    @Override
    public void onEvaluate(long index, int initPrec, int msd, int leastDigPos,
            String truncatedWholePart) {
        initPositions(initPrec, msd, leastDigPos, truncatedWholePart);

        if (mStoreToMemoryRequested) {
            mEvaluator.copyToMemory(index);
            mStoreToMemoryRequested = false;
        }
        redisplay();
    }

    /**
     * Store the result for this index if it is available.
     * If it is unavailable, set mStoreToMemoryRequested to indicate that we should store
     * when evaluation is complete.
     */
    public void onMemoryStore() {
        if (mEvaluator.hasResult(mIndex)) {
            mEvaluator.copyToMemory(mIndex);
        } else {
            mStoreToMemoryRequested = true;
            mEvaluator.requireResult(mIndex, this /* listener */, this /* CharMetricsInfo */);
        }
    }

    /**
     * Add the result to the value currently in memory.
     */
    public void onMemoryAdd() {
        mEvaluator.addToMemory(mIndex);
    }

    /**
     * Subtract the result from the value currently in memory.
     */
    public void onMemorySubtract() {
        mEvaluator.subtractFromMemory(mIndex);
    }

    /**
     * Set up scroll bounds (mMinPos, mMaxPos, etc.) and determine whether the result is
     * scrollable, based on the supplied information about the result.
     * This is unfortunately complicated because we need to predict whether trailing digits
     * will eventually be replaced by an exponent.
     * Just appending the exponent during formatting would be simpler, but would produce
     * jumpier results during transitions.
     * Only called from UI thread.
     */
    private void initPositions(int initPrecOffset, int msdIndex, int lsdOffset,
            String truncatedWholePart) {
        int maxChars = getMaxChars();
        mWholeLen = truncatedWholePart.length();
        // Allow a tiny amount of slop for associativity/rounding differences in length
        // calculation.  If getPreferredPrec() decided it should fit, we want to make it fit, too.
        // We reserved one extra pixel, so the extra length is OK.
        final int nSeparatorChars = (int) Math.ceil(
                separatorChars(truncatedWholePart, truncatedWholePart.length())
                - getNoEllipsisCredit() - 0.0001f);
        mWholePartFits = mWholeLen + nSeparatorChars <= maxChars;
        mLastPos = INVALID;
        mLsdOffset = lsdOffset;
        mAppendExponent = false;
        // Prevent scrolling past initial position, which is calculated to show leading digits.
        mCurrentPos = mMinPos = (int) Math.round(initPrecOffset * mCharWidth);
        if (msdIndex == Evaluator.INVALID_MSD) {
            // Possible zero value
            if (lsdOffset == Integer.MIN_VALUE) {
                // Definite zero value.
                mMaxPos = mMinPos;
                mMaxCharOffset = (int) Math.round(mMaxPos/mCharWidth);
                mScrollable = false;
            } else {
                // May be very small nonzero value.  Allow user to find out.
                mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL;
                mMinPos -= mCharWidth;  // Allow for future minus sign.
                mScrollable = true;
            }
            return;
        }
        int negative = truncatedWholePart.charAt(0) == '-' ? 1 : 0;
        if (msdIndex > mWholeLen && msdIndex <= mWholeLen + 3) {
            // Avoid tiny negative exponent; pretend msdIndex is just to the right of decimal point.
            msdIndex = mWholeLen - 1;
        }
        // Set to position of leftmost significant digit relative to dec. point. Usually negative.
        int minCharOffset = msdIndex - mWholeLen;
        if (minCharOffset > -1 && minCharOffset < MAX_LEADING_ZEROES + 2) {
            // Small number of leading zeroes, avoid scientific notation.
            minCharOffset = -1;
        }
        if (lsdOffset < MAX_RIGHT_SCROLL) {
            mMaxCharOffset = lsdOffset;
            if (mMaxCharOffset < -1 && mMaxCharOffset > -(MAX_TRAILING_ZEROES + 2)) {
                mMaxCharOffset = -1;
            }
            // lsdOffset is positive or negative, never 0.
            int currentExpLen = 0;  // Length of required standard scientific notation exponent.
            if (mMaxCharOffset < -1) {
                currentExpLen = expLen(-minCharOffset - 1);
            } else if (minCharOffset > -1 || mMaxCharOffset >= maxChars) {
                // Number is either entirely to the right of decimal point, or decimal point is
                // not visible when scrolled to the right.
                currentExpLen = expLen(-minCharOffset);
            }
            // Exponent length does not included added decimal point.  But whenever we add a
            // decimal point, we allow an extra character (SCI_NOTATION_EXTRA).
            final int separatorLength = mWholePartFits && minCharOffset < -3 ? nSeparatorChars : 0;
            mScrollable = (mMaxCharOffset + currentExpLen + separatorLength - minCharOffset
                    + negative >= maxChars);
            // Now adjust mMaxCharOffset for any required exponent.
            int newMaxCharOffset;
            if (currentExpLen > 0) {
                if (mScrollable) {
                    // We'll use exponent corresponding to leastDigPos when scrolled to right.
                    newMaxCharOffset = mMaxCharOffset + expLen(-lsdOffset);
                } else {
                    newMaxCharOffset = mMaxCharOffset + currentExpLen;
                }
                if (mMaxCharOffset <= -1 && newMaxCharOffset > -1) {
                    // Very unlikely; just drop exponent.
                    mMaxCharOffset = -1;
                } else {
                    mMaxCharOffset = Math.min(newMaxCharOffset, MAX_RIGHT_SCROLL);
                }
                mMaxPos = Math.min((int) Math.round(mMaxCharOffset * mCharWidth),
                        MAX_RIGHT_SCROLL);
            } else if (!mWholePartFits && !mScrollable) {
                // Corner case in which entire number fits, but not with grouping separators.  We
                // will use an exponent in un-scrolled position, which may hide digits.  Scrolling
                // by one character will remove the exponent and reveal the last digits.  Note
                // that in the forced scientific notation case, the exponent length is not
                // factored into mMaxCharOffset, since we do not want such an increase to impact
                // scrolling behavior.  In the unscrollable case, we thus have to append the
                // exponent at the end using the forcePrecision argument to formatResult, in order
                // to ensure that we get the entire result.
                mScrollable = (mMaxCharOffset + expLen(-minCharOffset - 1) - minCharOffset
                        + negative >= maxChars);
                if (mScrollable) {
                    mMaxPos = (int) Math.ceil(mMinPos + mCharWidth);
                    // Single character scroll will remove exponent and show remaining piece.
                } else {
                    mMaxPos = mMinPos;
                    mAppendExponent = true;
                }
            } else {
                mMaxPos = Math.min((int) Math.round(mMaxCharOffset * mCharWidth),
                        MAX_RIGHT_SCROLL);
            }
            if (!mScrollable) {
                // Position the number consistently with our assumptions to make sure it
                // actually fits.
                mCurrentPos = mMaxPos;
            }
        } else {
            mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL;
            mScrollable = true;
        }
    }

    /**
     * Display error message indicated by resourceId.
     * UI thread only.
     */
    @Override
    public void onError(long index, int resourceId) {
        mStoreToMemoryRequested = false;
        mValid = false;
        setLongClickable(false);
        mScrollable = false;
        final String msg = getContext().getString(resourceId);
        final float measuredWidth = Layout.getDesiredWidth(msg, getPaint());
        if (measuredWidth > mWidthConstraint) {
            // Multiply by .99 to avoid rounding effects.
            final float scaleFactor = 0.99f * mWidthConstraint / measuredWidth;
            final RelativeSizeSpan smallTextSpan = new RelativeSizeSpan(scaleFactor);
            final SpannableString scaledMsg = new SpannableString(msg);
            scaledMsg.setSpan(smallTextSpan, 0, msg.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            setText(scaledMsg);
        } else {
            setText(msg);
        }
    }

    private final int MAX_COPY_SIZE = 1000000;

    /*
     * Return the most significant digit position in the given string or Evaluator.INVALID_MSD.
     * Unlike Evaluator.getMsdIndexOf, we treat a final 1 as significant.
     * Pure function; callable from anywhere.
     */
    public static int getNaiveMsdIndexOf(String s) {
        final int len = s.length();
        for (int i = 0; i < len; ++i) {
            char c = s.charAt(i);
            if (c != '-' && c != '.' && c != '0') {
                return i;
            }
        }
        return Evaluator.INVALID_MSD;
    }

    /**
     * Format a result returned by Evaluator.getString() into a single line containing ellipses
     * (if appropriate) and an exponent (if appropriate).
     * We add two distinct kinds of exponents:
     * (1) If the final result contains the leading digit we use standard scientific notation.
     * (2) If not, we add an exponent corresponding to an interpretation of the final result as
     *     an integer.
     * We add an ellipsis on the left if the result was truncated.
     * We add ellipses and exponents in a way that leaves most digits in the position they
     * would have been in had we not done so. This minimizes jumps as a result of scrolling.
     * Result is NOT internationalized, uses "E" for exponent.
     * Called only from UI thread; We sometimes omit locking for fields.
     * @param precOffset The value that was passed to getString. Identifies the significance of
                the rightmost digit. A value of 1 means the rightmost digits corresponds to tenths.
     * @param maxDigs The maximum number of characters in the result
     * @param truncated The in parameter was already truncated, beyond possibly removing the
                minus sign.
     * @param negative The in parameter represents a negative result. (Minus sign may be removed
                without setting truncated.)
     * @param lastDisplayedOffset  If not null, we set lastDisplayedOffset[0] to the offset of
                the last digit actually appearing in the display.
     * @param forcePrecision If true, we make sure that the last displayed digit corresponds to
                precOffset, and allow maxDigs to be exceeded in adding the exponent and commas.
     * @param forceSciNotation Force scientific notation. May be set because we don't have
                space for grouping separators, but whole number otherwise fits.
     * @param insertCommas Insert commas (literally, not internationalized) as digit separators.
                We only ever do this for the integral part of a number, and only when no
                exponent is displayed in the initial position. The combination of which means
                that we only do it when no exponent is displayed.
                We insert commas in a way that does consider the width of the actual localized digit
                separator. Commas count towards maxDigs as the appropriate fraction of a digit.
     */
    private String formatResult(String in, int precOffset, int maxDigs, boolean truncated,
            boolean negative, int lastDisplayedOffset[], boolean forcePrecision,
            boolean forceSciNotation, boolean insertCommas) {
        final int minusSpace = negative ? 1 : 0;
        final int msdIndex = truncated ? -1 : getNaiveMsdIndexOf(in);  // INVALID_MSD is OK.
        String result = in;
        boolean needEllipsis = false;
        if (truncated || (negative && result.charAt(0) != '-')) {
            needEllipsis = true;
            result = KeyMaps.ELLIPSIS + result.substring(1, result.length());
            // Ellipsis may be removed again in the type(1) scientific notation case.
        }
        final int decIndex = result.indexOf('.');
        if (lastDisplayedOffset != null) {
            lastDisplayedOffset[0] = precOffset;
        }
        if (forceSciNotation || (decIndex == -1 || msdIndex != Evaluator.INVALID_MSD
                && msdIndex - decIndex > MAX_LEADING_ZEROES + 1) &&  precOffset != -1) {
            // Either:
            // 1) No decimal point displayed, and it's not just to the right of the last digit, or
            // 2) we are at the front of a number whos integral part is too large to allow
            // comma insertion, or
            // 3) we should suppress leading zeroes.
            // Add an exponent to let the user track which digits are currently displayed.
            // Start with type (2) exponent if we dropped no digits. -1 accounts for decimal point.
            // We currently never show digit separators together with an exponent.
            final int initExponent = precOffset > 0 ? -precOffset : -precOffset - 1;
            int exponent = initExponent;
            boolean hasPoint = false;
            if (!truncated && msdIndex < maxDigs - 1
                    && result.length() - msdIndex + 1 + minusSpace
                    <= maxDigs + SCI_NOTATION_EXTRA) {
                // Type (1) exponent computation and transformation:
                // Leading digit is in display window. Use standard calculator scientific notation
                // with one digit to the left of the decimal point. Insert decimal point and
                // delete leading zeroes.
                // We try to keep leading digits roughly in position, and never
                // lengthen the result by more than SCI_NOTATION_EXTRA.
                if (decIndex > msdIndex) {
                    // In the forceSciNotation, we can have a decimal point in the relevant digit
                    // range. Remove it.
                    result = result.substring(0, decIndex)
                            + result.substring(decIndex + 1, result.length());
                    // msdIndex and precOffset unaffected.
                }
                final int resLen = result.length();
                String fraction = result.substring(msdIndex + 1, resLen);
                result = (negative ? "-" : "") + result.substring(msdIndex, msdIndex + 1)
                        + "." + fraction;
                // Original exp was correct for decimal point at right of fraction.
                // Adjust by length of fraction.
                exponent = initExponent + resLen - msdIndex - 1;
                hasPoint = true;
            }
            // Exponent can't be zero.
            // Actually add the exponent of either type:
            if (!forcePrecision) {
                int dropDigits;  // Digits to drop to make room for exponent.
                if (hasPoint) {
                    // Type (1) exponent.
                    // Drop digits even if there is room. Otherwise the scrolling gets jumpy.
                    dropDigits = expLen(exponent);
                    if (dropDigits >= result.length() - 1) {
                        // Jumpy is better than no mantissa.  Probably impossible anyway.
                        dropDigits = Math.max(result.length() - 2, 0);
                    }
                } else {
                    // Type (2) exponent.
                    // Exponent depends on the number of digits we drop, which depends on
                    // exponent ...
                    for (dropDigits = 2; expLen(initExponent + dropDigits) > dropDigits;
                            ++dropDigits) {}
                    exponent = initExponent + dropDigits;
                    if (precOffset - dropDigits > mLsdOffset) {
                        // This can happen if e.g. result = 10^40 + 10^10
                        // It turns out we would otherwise display ...10e9 because it takes
                        // the same amount of space as ...1e10 but shows one more digit.
                        // But we don't want to display a trailing zero, even if it's free.
                        ++dropDigits;
                        ++exponent;
                    }
                }
                if (dropDigits >= result.length() - 1) {
                    // Display too small to show meaningful result.
                    return KeyMaps.ELLIPSIS + "E" + KeyMaps.ELLIPSIS;
                }
                result = result.substring(0, result.length() - dropDigits);
                if (lastDisplayedOffset != null) {
                    lastDisplayedOffset[0] -= dropDigits;
                }
            }
            result = result + "E" + Integer.toString(exponent);
        } else if (insertCommas) {
            // Add commas to the whole number section, and then truncate on left to fit,
            // counting commas as a fractional digit.
            final int wholeStart = needEllipsis ? 1 : 0;
            int orig_length = result.length();
            final float nCommaChars;
            if (decIndex != -1) {
                nCommaChars = separatorChars(result, decIndex);
                result = StringUtils.addCommas(result, wholeStart, decIndex)
                        + result.substring(decIndex, orig_length);
            } else {
                nCommaChars = separatorChars(result, orig_length);
                result = StringUtils.addCommas(result, wholeStart, orig_length);
            }
            if (needEllipsis) {
                orig_length -= 1;  // Exclude ellipsis.
            }
            final float len = orig_length + nCommaChars;
            int deletedChars = 0;
            final float ellipsisCredit = getNoEllipsisCredit();
            final float decimalCredit = getDecimalCredit();
            final float effectiveLen = len - (decIndex == -1 ? 0 : getDecimalCredit());
            final float ellipsisAdjustment =
                    needEllipsis ? mNoExponentCredit : getNoEllipsisCredit();
            // As above, we allow for a tiny amount of extra length here, for consistency with
            // getPreferredPrec().
            if (effectiveLen - ellipsisAdjustment > (float) (maxDigs - wholeStart) + 0.0001f
                && !forcePrecision) {
                float deletedWidth = 0.0f;
                while (effectiveLen - mNoExponentCredit - deletedWidth
                        > (float) (maxDigs - 1 /* for ellipsis */)) {
                    if (result.charAt(deletedChars) == ',') {
                        deletedWidth += mGroupingSeparatorWidthRatio;
                    } else {
                        deletedWidth += 1.0f;
                    }
                    deletedChars++;
                }
            }
            if (deletedChars > 0) {
                result = KeyMaps.ELLIPSIS + result.substring(deletedChars, result.length());
            } else if (needEllipsis) {
                result = KeyMaps.ELLIPSIS + result;
            }
        }
        return result;
    }

    /**
     * Get formatted, but not internationalized, result from mEvaluator.
     * @param precOffset requested position (1 = tenths) of last included digit
     * @param maxSize maximum number of characters (more or less) in result
     * @param lastDisplayedOffset zeroth entry is set to actual offset of last included digit,
     *                            after adjusting for exponent, etc.  May be null.
     * @param forcePrecision Ensure that last included digit is at pos, at the expense
     *                       of treating maxSize as a soft limit.
     * @param forceSciNotation Force scientific notation, even if not required by maxSize.
     * @param insertCommas Insert commas as digit separators.
     */
    private String getFormattedResult(int precOffset, int maxSize, int lastDisplayedOffset[],
            boolean forcePrecision, boolean forceSciNotation, boolean insertCommas) {
        final boolean truncated[] = new boolean[1];
        final boolean negative[] = new boolean[1];
        final int requestedPrecOffset[] = {precOffset};
        final String rawResult = mEvaluator.getString(mIndex, requestedPrecOffset, mMaxCharOffset,
                maxSize, truncated, negative, this);
        return formatResult(rawResult, requestedPrecOffset[0], maxSize, truncated[0], negative[0],
                lastDisplayedOffset, forcePrecision, forceSciNotation, insertCommas);
   }

    /**
     * Return entire result (within reason) up to current displayed precision.
     * @param withSeparators  Add digit separators
     */
    public String getFullText(boolean withSeparators) {
        if (!mValid) return "";
        if (!mScrollable) return getText().toString();
        return KeyMaps.translateResult(getFormattedResult(mLastDisplayedOffset, MAX_COPY_SIZE,
                null, true /* forcePrecision */, false /* forceSciNotation */, withSeparators));
    }

    /**
     * Did the above produce a correct result?
     * UI thread only.
     */
    public boolean fullTextIsExact() {
        return !mScrollable || (getCharOffset(mMaxPos) == getCharOffset(mCurrentPos)
                && mMaxCharOffset != MAX_RIGHT_SCROLL);
    }

    /**
     * Get entire result up to current displayed precision, or up to MAX_COPY_EXTRA additional
     * digits, if it will lead to an exact result.
     */
    public String getFullCopyText() {
        if (!mValid
                || mLsdOffset == Integer.MAX_VALUE
                || fullTextIsExact()
                || mWholeLen > MAX_RECOMPUTE_DIGITS
                || mWholeLen + mLsdOffset > MAX_RECOMPUTE_DIGITS
                || mLsdOffset - mLastDisplayedOffset > MAX_COPY_EXTRA) {
            return getFullText(false /* withSeparators */);
        }
        // It's reasonable to compute and copy the exact result instead.
        int fractionLsdOffset = Math.max(0, mLsdOffset);
        String rawResult = mEvaluator.getResult(mIndex).toStringTruncated(fractionLsdOffset);
        if (mLsdOffset <= -1) {
            // Result has trailing decimal point. Remove it.
            rawResult = rawResult.substring(0, rawResult.length() - 1);
            fractionLsdOffset = -1;
        }
        final String formattedResult = formatResult(rawResult, fractionLsdOffset, MAX_COPY_SIZE,
                false, rawResult.charAt(0) == '-', null, true /* forcePrecision */,
                false /* forceSciNotation */, false /* insertCommas */);
        return KeyMaps.translateResult(formattedResult);
    }

    /**
     * Return the maximum number of characters that will fit in the result display.
     * May be called asynchronously from non-UI thread. From Evaluator.CharMetricsInfo.
     * Returns zero if measurement hasn't completed.
     */
    @Override
    public int getMaxChars() {
        int result;
        synchronized(mWidthLock) {
            return (int) Math.floor(mWidthConstraint / mCharWidth);
        }
    }

    /**
     * @return {@code true} if the currently displayed result is scrollable
     */
    public boolean isScrollable() {
        return mScrollable;
    }

    /**
     * Map pixel position to digit offset.
     * UI thread only.
     */
    int getCharOffset(int pos) {
        return (int) Math.round(pos / mCharWidth);  // Lock not needed.
    }

    void clear() {
        mValid = false;
        mScrollable = false;
        setText("");
        setLongClickable(false);
    }

    @Override
    public void onCancelled(long index) {
        clear();
        mStoreToMemoryRequested = false;
    }

    /**
     * Refresh display.
     * Only called in UI thread. Index argument is currently ignored.
     */
    @Override
    public void onReevaluate(long index) {
        redisplay();
    }

    public void redisplay() {
        int maxChars = getMaxChars();
        if (maxChars < 4) {
            // Display currently too small to display a reasonable result. Punt to avoid crash.
            return;
        }
        if (mScroller.isFinished() && length() > 0) {
            setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
        }
        int currentCharOffset = getCharOffset(mCurrentPos);
        int lastDisplayedOffset[] = new int[1];
        String result = getFormattedResult(currentCharOffset, maxChars, lastDisplayedOffset,
                mAppendExponent /* forcePrecision; preserve entire result */,
                !mWholePartFits
                &&  currentCharOffset == getCharOffset(mMinPos) /* forceSciNotation */,
                mWholePartFits /* insertCommas */ );
        int expIndex = result.indexOf('E');
        result = KeyMaps.translateResult(result);
        if (expIndex > 0 && result.indexOf('.') == -1) {
          // Gray out exponent if used as position indicator
            SpannableString formattedResult = new SpannableString(result);
            formattedResult.setSpan(mExponentColorSpan, expIndex, result.length(),
                                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            setText(formattedResult);
        } else {
            setText(result);
        }
        mLastDisplayedOffset = lastDisplayedOffset[0];
        mValid = true;
        setLongClickable(true);
    }

    @Override
    protected void onTextChanged(java.lang.CharSequence text, int start, int lengthBefore,
            int lengthAfter) {
        super.onTextChanged(text, start, lengthBefore, lengthAfter);

        if (!mScrollable || mScroller.isFinished()) {
            if (lengthBefore == 0 && lengthAfter > 0) {
                setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
                setContentDescription(null);
            } else if (lengthBefore > 0 && lengthAfter == 0) {
                setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_NONE);
                setContentDescription(getContext().getString(R.string.desc_result));
            }
        }
    }

    @Override
    public void computeScroll() {
        if (!mScrollable) {
            return;
        }

        if (mScroller.computeScrollOffset()) {
            mCurrentPos = mScroller.getCurrX();
            if (getCharOffset(mCurrentPos) != getCharOffset(mLastPos)) {
                mLastPos = mCurrentPos;
                redisplay();
            }
        }

        if (!mScroller.isFinished()) {
                postInvalidateOnAnimation();
                setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_NONE);
        } else if (length() > 0){
            setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
        }
    }

    /**
     * Use ActionMode for copy/memory support on M and higher.
     */
    @TargetApi(Build.VERSION_CODES.M)
    private void setupActionMode() {
        mCopyActionModeCallback = new ActionMode.Callback2() {

            @Override
            public boolean onCreateActionMode(ActionMode mode, Menu menu) {
                final MenuInflater inflater = mode.getMenuInflater();
                return createContextMenu(inflater, menu);
            }

            @Override
            public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
                return false; // Return false if nothing is done
            }

            @Override
            public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
                if (onMenuItemClick(item)) {
                    mode.finish();
                    return true;
                } else {
                    return false;
                }
            }

            @Override
            public void onDestroyActionMode(ActionMode mode) {
                unhighlightResult();
                mActionMode = null;
            }

            @Override
            public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
                super.onGetContentRect(mode, view, outRect);

                outRect.left += view.getPaddingLeft();
                outRect.top += view.getPaddingTop();
                outRect.right -= view.getPaddingRight();
                outRect.bottom -= view.getPaddingBottom();
                final int width = (int) Layout.getDesiredWidth(getText(), getPaint());
                if (width < outRect.width()) {
                    outRect.left = outRect.right - width;
                }

                if (!BuildCompat.isAtLeastN()) {
                    // The CAB (prior to N) only takes the translation of a view into account, so
                    // if a scale is applied to the view then the offset outRect will end up being
                    // positioned incorrectly. We workaround that limitation by manually applying
                    // the scale to the outRect, which the CAB will then offset to the correct
                    // position.
                    final float scaleX = view.getScaleX();
                    final float scaleY = view.getScaleY();
                    outRect.left *= scaleX;
                    outRect.right *= scaleX;
                    outRect.top *= scaleY;
                    outRect.bottom *= scaleY;
                }
            }
        };
        setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                if (mValid) {
                    mActionMode = startActionMode(mCopyActionModeCallback,
                            ActionMode.TYPE_FLOATING);
                    return true;
                }
                return false;
            }
        });
    }

    /**
     * Use ContextMenu for copy/memory support on L and lower.
     */
    private void setupContextMenu() {
        setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
            @Override
            public void onCreateContextMenu(ContextMenu contextMenu, View view,
                    ContextMenu.ContextMenuInfo contextMenuInfo) {
                final MenuInflater inflater = new MenuInflater(getContext());
                createContextMenu(inflater, contextMenu);
                mContextMenu = contextMenu;
                for (int i = 0; i < contextMenu.size(); i ++) {
                    contextMenu.getItem(i).setOnMenuItemClickListener(CalculatorResult.this);
                }
            }
        });
        setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                if (mValid) {
                    return showContextMenu();
                }
                return false;
            }
        });
    }

    private boolean createContextMenu(MenuInflater inflater, Menu menu) {
        inflater.inflate(R.menu.menu_result, menu);
        final boolean displayMemory = mEvaluator.getMemoryIndex() != 0;
        final MenuItem memoryAddItem = menu.findItem(R.id.memory_add);
        final MenuItem memorySubtractItem = menu.findItem(R.id.memory_subtract);
        memoryAddItem.setEnabled(displayMemory);
        memorySubtractItem.setEnabled(displayMemory);
        highlightResult();
        return true;
    }

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

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

    private void unhighlightResult() {
        final Spannable text = (Spannable) getText();
        text.removeSpan(mHighlightSpan);
    }

    private void setPrimaryClip(ClipData clip) {
        ClipboardManager clipboard = (ClipboardManager) getContext().
                                               getSystemService(Context.CLIPBOARD_SERVICE);
        clipboard.setPrimaryClip(clip);
    }

    private void copyContent() {
        final CharSequence text = getFullCopyText();
        ClipboardManager clipboard =
                (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
        // We include a tag URI, to allow us to recognize our own results and handle them
        // specially.
        ClipData.Item newItem = new ClipData.Item(text, null, mEvaluator.capture(mIndex));
        String[] mimeTypes = new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN};
        ClipData cd = new ClipData("calculator result", mimeTypes, newItem);
        clipboard.setPrimaryClip(cd);
        Toast.makeText(getContext(), R.string.text_copied_toast, Toast.LENGTH_SHORT).show();
    }

    @Override
    public boolean onMenuItemClick(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.memory_add:
                onMemoryAdd();
                return true;
            case R.id.memory_subtract:
                onMemorySubtract();
                return true;
            case R.id.memory_store:
                onMemoryStore();
                return true;
            case R.id.menu_copy:
                if (mEvaluator.evaluationInProgress(mIndex)) {
                    // Refuse to copy placeholder characters.
                    return false;
                } else {
                    copyContent();
                    unhighlightResult();
                    return true;
                }
            default:
                return false;
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        stopActionModeOrContextMenu();
        super.onDetachedFromWindow();
    }
}