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