am 24a929d7: am fc231ad6: am 70f49ecd: am 9d75c1ac: am 8a4f81c5: More correctly pronounce advanced operators in Talkback

* commit '24a929d77f05a4a708a90ae4c0db565a8b57707d':
  More correctly pronounce advanced operators in Talkback
diff --git a/src/com/android/calculator2/Calculator.java b/src/com/android/calculator2/Calculator.java
index f1c9835..4eecb50 100644
--- a/src/com/android/calculator2/Calculator.java
+++ b/src/com/android/calculator2/Calculator.java
@@ -14,8 +14,6 @@
  * limitations under the License.
  */
 
-// TODO: Better indication of when the result is known to be exact.
-// TODO: Check and possibly fix accessability issues.
 // TODO: Copy & more general paste in formula?  Note that this requires
 //       great care: Currently the text version of a displayed formula
 //       is not directly useful for re-evaluating the formula later, since
@@ -44,8 +42,10 @@
 import android.support.annotation.NonNull;
 import android.support.v4.view.ViewPager;
 import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
 import android.text.Spanned;
 import android.text.style.ForegroundColorSpan;
+import android.text.TextUtils;
 import android.util.Property;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
@@ -210,9 +210,13 @@
     private View mCurrentButton;
     private Animator mCurrentAnimator;
 
-    private String mUnprocessedChars = null;   // Characters that were recently entered
-                                               // at the end of the display that have not yet
-                                               // been added to the underlying expression.
+    // Characters that were recently entered at the end of the display that have not yet
+    // been added to the underlying expression.
+    private String mUnprocessedChars = null;
+
+    // Color to highlight unprocessed characters from physical keyboard.
+    // TODO: should probably match this to the error color?
+    private ForegroundColorSpan mUnprocessedColorSpan = new ForegroundColorSpan(Color.RED);
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -536,18 +540,13 @@
     }
 
     void redisplayFormula() {
-        String formula = mEvaluator.getExpr().toString(this);
+        SpannableStringBuilder formula = mEvaluator.getExpr().toSpannableStringBuilder(this);
         if (mUnprocessedChars != null) {
             // Add and highlight characters we couldn't process.
-            SpannableString formatted = new SpannableString(formula + mUnprocessedChars);
-            // TODO: should probably match this to the error color.
-            formatted.setSpan(new ForegroundColorSpan(Color.RED),
-                              formula.length(), formatted.length(),
-                              Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-            mFormulaText.changeTextTo(formatted);
-        } else {
-            mFormulaText.changeTextTo(formula);
+            formula.append(mUnprocessedChars, mUnprocessedColorSpan,
+                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
         }
+        mFormulaText.changeTextTo(formula);
     }
 
     @Override
diff --git a/src/com/android/calculator2/CalculatorExpr.java b/src/com/android/calculator2/CalculatorExpr.java
index 3023b5c..8a008b8 100644
--- a/src/com/android/calculator2/CalculatorExpr.java
+++ b/src/com/android/calculator2/CalculatorExpr.java
@@ -21,6 +21,11 @@
 import com.hp.creals.UnaryCRFunction;
 
 import android.content.Context;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.TtsSpan;
+import android.text.style.TtsSpan.TextBuilder;
 import android.util.Log;
 
 import java.math.BigInteger;
@@ -46,11 +51,19 @@
 
     private static abstract class Token {
         abstract TokenKind kind();
+
+        /**
+         * Write kind as Byte followed by data needed by subclass constructor.
+         */
         abstract void write(DataOutput out) throws IOException;
-                // Implementation writes kind as Byte followed by
-                // data read by constructor.
-        abstract String toString(Context context);
-                // We need the context to convert button ids to strings.
+
+        /**
+         * Return a textual representation of the token.
+         * The result is suitable for either display as part od the formula or TalkBack use.
+         * It may be a SpannableString that includes added TalkBack information.
+         * @param context context used for converting button ids to strings
+         */
+        abstract CharSequence toCharSequence(Context context);
     }
 
     // An operator token
@@ -68,8 +81,16 @@
             out.writeInt(mId);
         }
         @Override
-        public String toString(Context context) {
-            return KeyMaps.toString(context, mId);
+        public CharSequence toCharSequence(Context context) {
+            String desc = KeyMaps.toDescriptiveString(context, mId);
+            if (desc != null) {
+                SpannableString result = new SpannableString(KeyMaps.toString(context, mId));
+                Object descSpan = new TtsSpan.TextBuilder(desc).build();
+                result.setSpan(descSpan, 0, result.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+                return result;
+            } else {
+                return KeyMaps.toString(context, mId);
+            }
         }
         @Override
         TokenKind kind() { return TokenKind.OPERATOR; }
@@ -193,7 +214,7 @@
         }
 
         @Override
-        String toString(Context context) {
+        CharSequence toCharSequence(Context context) {
             return toString();
         }
 
@@ -323,7 +344,7 @@
             }
         }
         @Override
-        String toString(Context context) {
+        CharSequence toCharSequence(Context context) {
             return KeyMaps.translateResult(mShortRep);
         }
         @Override
@@ -1019,11 +1040,11 @@
     }
 
     // Produce a string representation of the expression itself
-    String toString(Context context) {
-        StringBuilder sb = new StringBuilder();
+    SpannableStringBuilder toSpannableStringBuilder(Context context) {
+        SpannableStringBuilder ssb = new SpannableStringBuilder();
         for (Token t: mExpr) {
-            sb.append(t.toString(context));
+            ssb.append(t.toCharSequence(context));
         }
-        return sb.toString();
+        return ssb;
     }
 }
diff --git a/src/com/android/calculator2/CalculatorText.java b/src/com/android/calculator2/CalculatorText.java
index 4b0b0c9..109c2af 100644
--- a/src/com/android/calculator2/CalculatorText.java
+++ b/src/com/android/calculator2/CalculatorText.java
@@ -224,11 +224,22 @@
      * Otherwise, e.g. after deletion, announce the entire new text.
      */
     public void changeTextTo(CharSequence newText) {
-        CharSequence oldText = getText();
+        final CharSequence oldText = getText();
         if (startsWith(newText, oldText)) {
-            int newLen = newText.length();
-            int oldLen = oldText.length();
-            if (oldLen != newLen) {
+            final int newLen = newText.length();
+            final int oldLen = oldText.length();
+            if (newLen == oldLen + 1) {
+                // The algorithm for pronouncing a single character doesn't seem
+                // to respect our hints.  Don't give it the choice.
+                final char c = newText.charAt(oldLen);
+                final int id = KeyMaps.keyForChar(c);
+                final String descr = KeyMaps.toDescriptiveString(getContext(), id);
+                if (descr != null) {
+                    announceForAccessibility(descr);
+                } else {
+                    announceForAccessibility(String.valueOf(c));
+                }
+            } else if (newLen > oldLen) {
                 announceForAccessibility(newText.subSequence(oldLen, newLen));
             }
         } else {
diff --git a/src/com/android/calculator2/KeyMaps.java b/src/com/android/calculator2/KeyMaps.java
index b4bfdf9..f99850b 100644
--- a/src/com/android/calculator2/KeyMaps.java
+++ b/src/com/android/calculator2/KeyMaps.java
@@ -115,6 +115,57 @@
     }
 
     /**
+     * Map key id to corresponding (internationalized) descriptive string that can be used
+     * to correctly read back a formula.
+     * Only used for operators and individual characters; not used inside constants.
+     * Returns null when we don't need a descriptive string.
+     * Pure function.
+     */
+    public static String toDescriptiveString(Context context, int id) {
+        switch(id) {
+            case R.id.op_fact:
+                return context.getString(R.string.desc_op_fact);
+            case R.id.fun_sin:
+                return context.getString(R.string.desc_fun_sin)
+                        + " " + context.getString(R.string.desc_lparen);
+            case R.id.fun_cos:
+                return context.getString(R.string.desc_fun_cos)
+                        + " " + context.getString(R.string.desc_lparen);
+            case R.id.fun_tan:
+                return context.getString(R.string.desc_fun_tan)
+                        + " " + context.getString(R.string.desc_lparen);
+            case R.id.fun_arcsin:
+                return context.getString(R.string.desc_fun_arcsin)
+                        + " " + context.getString(R.string.desc_lparen);
+            case R.id.fun_arccos:
+                return context.getString(R.string.desc_fun_arccos)
+                        + " " + context.getString(R.string.desc_lparen);
+            case R.id.fun_arctan:
+                return context.getString(R.string.desc_fun_arctan)
+                        + " " + context.getString(R.string.desc_lparen);
+            case R.id.fun_ln:
+                return context.getString(R.string.desc_fun_ln)
+                        + " " + context.getString(R.string.desc_lparen);
+            case R.id.fun_log:
+                return context.getString(R.string.desc_fun_log)
+                        + " " + context.getString(R.string.desc_lparen);
+            case R.id.fun_exp:
+                return context.getString(R.string.desc_fun_exp)
+                        + " " + context.getString(R.string.desc_lparen);
+            case R.id.lparen:
+                return context.getString(R.string.desc_lparen);
+            case R.id.rparen:
+                return context.getString(R.string.desc_rparen);
+            case R.id.op_pow:
+                return context.getString(R.string.desc_op_pow);
+            case R.id.dec_point:
+                return context.getString(R.string.desc_dec_point);
+            default:
+                return null;
+        }
+    }
+
+    /**
      * Does a button id correspond to a binary operator?
      * Pure function.
      */