1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.calculator2; 18 19 import android.content.res.Resources; 20 import android.content.Context; 21 import android.app.Activity; 22 import android.util.Log; 23 import android.view.View; 24 import android.widget.Button; 25 26 import java.text.DecimalFormatSymbols; 27 import java.util.HashMap; 28 import java.util.Locale; 29 30 /** 31 * Collection of mapping functions between key ids, characters, internationalized 32 * and non-internationalized characters, etc. 33 * <p> 34 * KeyMap instances are not meaningful; everything here is static. 35 * All functions are either pure, or are assumed to be called only from a single UI thread. 36 */ 37 public class KeyMaps { 38 /** 39 * Map key id to corresponding (internationalized) display string. 40 * Pure function. 41 */ 42 public static String toString(Context context, int id) { 43 switch(id) { 44 case R.id.const_pi: 45 return context.getString(R.string.const_pi); 46 case R.id.const_e: 47 return context.getString(R.string.const_e); 48 case R.id.op_sqrt: 49 return context.getString(R.string.op_sqrt); 50 case R.id.op_fact: 51 return context.getString(R.string.op_fact); 52 case R.id.op_pct: 53 return context.getString(R.string.op_pct); 54 case R.id.fun_sin: 55 return context.getString(R.string.fun_sin) + context.getString(R.string.lparen); 56 case R.id.fun_cos: 57 return context.getString(R.string.fun_cos) + context.getString(R.string.lparen); 58 case R.id.fun_tan: 59 return context.getString(R.string.fun_tan) + context.getString(R.string.lparen); 60 case R.id.fun_arcsin: 61 return context.getString(R.string.fun_arcsin) + context.getString(R.string.lparen); 62 case R.id.fun_arccos: 63 return context.getString(R.string.fun_arccos) + context.getString(R.string.lparen); 64 case R.id.fun_arctan: 65 return context.getString(R.string.fun_arctan) + context.getString(R.string.lparen); 66 case R.id.fun_ln: 67 return context.getString(R.string.fun_ln) + context.getString(R.string.lparen); 68 case R.id.fun_log: 69 return context.getString(R.string.fun_log) + context.getString(R.string.lparen); 70 case R.id.fun_exp: 71 // Button label doesn't work. 72 return context.getString(R.string.exponential) + context.getString(R.string.lparen); 73 case R.id.lparen: 74 return context.getString(R.string.lparen); 75 case R.id.rparen: 76 return context.getString(R.string.rparen); 77 case R.id.op_pow: 78 return context.getString(R.string.op_pow); 79 case R.id.op_mul: 80 return context.getString(R.string.op_mul); 81 case R.id.op_div: 82 return context.getString(R.string.op_div); 83 case R.id.op_add: 84 return context.getString(R.string.op_add); 85 case R.id.op_sqr: 86 // Button label doesn't work. 87 return context.getString(R.string.squared); 88 case R.id.op_sub: 89 return context.getString(R.string.op_sub); 90 case R.id.dec_point: 91 return context.getString(R.string.dec_point); 92 case R.id.digit_0: 93 return context.getString(R.string.digit_0); 94 case R.id.digit_1: 95 return context.getString(R.string.digit_1); 96 case R.id.digit_2: 97 return context.getString(R.string.digit_2); 98 case R.id.digit_3: 99 return context.getString(R.string.digit_3); 100 case R.id.digit_4: 101 return context.getString(R.string.digit_4); 102 case R.id.digit_5: 103 return context.getString(R.string.digit_5); 104 case R.id.digit_6: 105 return context.getString(R.string.digit_6); 106 case R.id.digit_7: 107 return context.getString(R.string.digit_7); 108 case R.id.digit_8: 109 return context.getString(R.string.digit_8); 110 case R.id.digit_9: 111 return context.getString(R.string.digit_9); 112 default: 113 return ""; 114 } 115 } 116 117 /** 118 * Map key id to corresponding (internationalized) descriptive string that can be used 119 * to correctly read back a formula. 120 * Only used for operators and individual characters; not used inside constants. 121 * Returns null when we don't need a descriptive string. 122 * Pure function. 123 */ 124 public static String toDescriptiveString(Context context, int id) { 125 switch(id) { 126 case R.id.op_fact: 127 return context.getString(R.string.desc_op_fact); 128 case R.id.fun_sin: 129 return context.getString(R.string.desc_fun_sin) 130 + " " + context.getString(R.string.desc_lparen); 131 case R.id.fun_cos: 132 return context.getString(R.string.desc_fun_cos) 133 + " " + context.getString(R.string.desc_lparen); 134 case R.id.fun_tan: 135 return context.getString(R.string.desc_fun_tan) 136 + " " + context.getString(R.string.desc_lparen); 137 case R.id.fun_arcsin: 138 return context.getString(R.string.desc_fun_arcsin) 139 + " " + context.getString(R.string.desc_lparen); 140 case R.id.fun_arccos: 141 return context.getString(R.string.desc_fun_arccos) 142 + " " + context.getString(R.string.desc_lparen); 143 case R.id.fun_arctan: 144 return context.getString(R.string.desc_fun_arctan) 145 + " " + context.getString(R.string.desc_lparen); 146 case R.id.fun_ln: 147 return context.getString(R.string.desc_fun_ln) 148 + " " + context.getString(R.string.desc_lparen); 149 case R.id.fun_log: 150 return context.getString(R.string.desc_fun_log) 151 + " " + context.getString(R.string.desc_lparen); 152 case R.id.fun_exp: 153 return context.getString(R.string.desc_fun_exp) 154 + " " + context.getString(R.string.desc_lparen); 155 case R.id.lparen: 156 return context.getString(R.string.desc_lparen); 157 case R.id.rparen: 158 return context.getString(R.string.desc_rparen); 159 case R.id.op_pow: 160 return context.getString(R.string.desc_op_pow); 161 case R.id.dec_point: 162 return context.getString(R.string.desc_dec_point); 163 default: 164 return null; 165 } 166 } 167 168 /** 169 * Does a button id correspond to a binary operator? 170 * Pure function. 171 */ 172 public static boolean isBinary(int id) { 173 switch(id) { 174 case R.id.op_pow: 175 case R.id.op_mul: 176 case R.id.op_div: 177 case R.id.op_add: 178 case R.id.op_sub: 179 return true; 180 default: 181 return false; 182 } 183 } 184 185 /** 186 * Does a button id correspond to a function that introduces an implicit lparen? 187 * Pure function. 188 */ 189 public static boolean isFunc(int id) { 190 switch(id) { 191 case R.id.fun_sin: 192 case R.id.fun_cos: 193 case R.id.fun_tan: 194 case R.id.fun_arcsin: 195 case R.id.fun_arccos: 196 case R.id.fun_arctan: 197 case R.id.fun_ln: 198 case R.id.fun_log: 199 case R.id.fun_exp: 200 return true; 201 default: 202 return false; 203 } 204 } 205 206 /** 207 * Does a button id correspond to a prefix operator? 208 * Pure function. 209 */ 210 public static boolean isPrefix(int id) { 211 switch(id) { 212 case R.id.op_sqrt: 213 case R.id.op_sub: 214 return true; 215 default: 216 return false; 217 } 218 } 219 220 /** 221 * Does a button id correspond to a suffix operator? 222 */ 223 public static boolean isSuffix(int id) { 224 switch (id) { 225 case R.id.op_fact: 226 case R.id.op_pct: 227 case R.id.op_sqr: 228 return true; 229 default: 230 return false; 231 } 232 } 233 234 public static final int NOT_DIGIT = 10; 235 236 public static final String ELLIPSIS = "\u2026"; 237 238 public static final char MINUS_SIGN = '\u2212'; 239 240 /** 241 * Map key id to digit or NOT_DIGIT 242 * Pure function. 243 */ 244 public static int digVal(int id) { 245 switch (id) { 246 case R.id.digit_0: 247 return 0; 248 case R.id.digit_1: 249 return 1; 250 case R.id.digit_2: 251 return 2; 252 case R.id.digit_3: 253 return 3; 254 case R.id.digit_4: 255 return 4; 256 case R.id.digit_5: 257 return 5; 258 case R.id.digit_6: 259 return 6; 260 case R.id.digit_7: 261 return 7; 262 case R.id.digit_8: 263 return 8; 264 case R.id.digit_9: 265 return 9; 266 default: 267 return NOT_DIGIT; 268 } 269 } 270 271 /** 272 * Map digit to corresponding key. Inverse of above. 273 * Pure function. 274 */ 275 public static int keyForDigVal(int v) { 276 switch(v) { 277 case 0: 278 return R.id.digit_0; 279 case 1: 280 return R.id.digit_1; 281 case 2: 282 return R.id.digit_2; 283 case 3: 284 return R.id.digit_3; 285 case 4: 286 return R.id.digit_4; 287 case 5: 288 return R.id.digit_5; 289 case 6: 290 return R.id.digit_6; 291 case 7: 292 return R.id.digit_7; 293 case 8: 294 return R.id.digit_8; 295 case 9: 296 return R.id.digit_9; 297 default: 298 return View.NO_ID; 299 } 300 } 301 302 // The following two are only used for recognizing additional 303 // input characters from a physical keyboard. They are not used 304 // for output internationalization. 305 private static char mDecimalPt; 306 307 private static char mPiChar; 308 309 /** 310 * Character used as a placeholder for digits that are currently unknown in a result that 311 * is being computed. We initially generate blanks, and then use this as a replacement 312 * during final translation. 313 * <p/> 314 * Note: the character must correspond closely to the width of a digit, 315 * otherwise the UI will visibly shift once the computation is finished. 316 */ 317 private static final char CHAR_DIGIT_UNKNOWN = '\u2007'; 318 319 /** 320 * Map typed function name strings to corresponding button ids. 321 * We (now redundantly?) include both localized and English names. 322 */ 323 private static HashMap<String, Integer> sKeyValForFun; 324 325 /** 326 * Result string corresponding to a character in the calculator result. 327 * The string values in the map are expected to be one character long. 328 */ 329 private static HashMap<Character, String> sOutputForResultChar; 330 331 /** 332 * Locale string corresponding to preceding map and character constants. 333 * We recompute the map if this is not the current locale. 334 */ 335 private static String sLocaleForMaps = "none"; 336 337 /** 338 * Activity to use for looking up buttons. 339 */ 340 private static Activity mActivity; 341 342 /** 343 * Set acttivity used for looking up button labels. 344 * Call only from UI thread. 345 */ 346 public static void setActivity(Activity a) { 347 mActivity = a; 348 } 349 350 /** 351 * Return the button id corresponding to the supplied character or return NO_ID. 352 * Called only by UI thread. 353 */ 354 public static int keyForChar(char c) { 355 validateMaps(); 356 if (Character.isDigit(c)) { 357 int i = Character.digit(c, 10); 358 return KeyMaps.keyForDigVal(i); 359 } 360 switch (c) { 361 case '.': 362 case ',': 363 return R.id.dec_point; 364 case '-': 365 case MINUS_SIGN: 366 return R.id.op_sub; 367 case '+': 368 return R.id.op_add; 369 case '*': 370 case '\u00D7': // MULTIPLICATION SIGN 371 return R.id.op_mul; 372 case '/': 373 case '\u00F7': // DIVISION SIGN 374 return R.id.op_div; 375 // We no longer localize function names, so they can't start with an 'e' or 'p'. 376 case 'e': 377 case 'E': 378 return R.id.const_e; 379 case 'p': 380 case 'P': 381 return R.id.const_pi; 382 case '^': 383 return R.id.op_pow; 384 case '!': 385 return R.id.op_fact; 386 case '%': 387 return R.id.op_pct; 388 case '(': 389 return R.id.lparen; 390 case ')': 391 return R.id.rparen; 392 default: 393 if (c == mDecimalPt) return R.id.dec_point; 394 if (c == mPiChar) return R.id.const_pi; 395 // pi is not translated, but it might be typable on a Greek keyboard, 396 // or pasted in, so we check ... 397 return View.NO_ID; 398 } 399 } 400 401 /** 402 * Add information corresponding to the given button id to sKeyValForFun, to be used 403 * when mapping keyboard input to button ids. 404 */ 405 static void addButtonToFunMap(int button_id) { 406 Button button = (Button)mActivity.findViewById(button_id); 407 sKeyValForFun.put(button.getText().toString(), button_id); 408 } 409 410 /** 411 * Add information corresponding to the given button to sOutputForResultChar, to be used 412 * when translating numbers on output. 413 */ 414 static void addButtonToOutputMap(char c, int button_id) { 415 Button button = (Button)mActivity.findViewById(button_id); 416 sOutputForResultChar.put(c, button.getText().toString()); 417 } 418 419 // Ensure that the preceding map and character constants are 420 // initialized and correspond to the current locale. 421 // Called only by a single thread, namely the UI thread. 422 static void validateMaps() { 423 Locale locale = Locale.getDefault(); 424 String lname = locale.toString(); 425 if (lname != sLocaleForMaps) { 426 Log.v ("Calculator", "Setting local to: " + lname); 427 sKeyValForFun = new HashMap<String, Integer>(); 428 sKeyValForFun.put("sin", R.id.fun_sin); 429 sKeyValForFun.put("cos", R.id.fun_cos); 430 sKeyValForFun.put("tan", R.id.fun_tan); 431 sKeyValForFun.put("arcsin", R.id.fun_arcsin); 432 sKeyValForFun.put("arccos", R.id.fun_arccos); 433 sKeyValForFun.put("arctan", R.id.fun_arctan); 434 sKeyValForFun.put("asin", R.id.fun_arcsin); 435 sKeyValForFun.put("acos", R.id.fun_arccos); 436 sKeyValForFun.put("atan", R.id.fun_arctan); 437 sKeyValForFun.put("ln", R.id.fun_ln); 438 sKeyValForFun.put("log", R.id.fun_log); 439 sKeyValForFun.put("sqrt", R.id.op_sqrt); // special treatment 440 addButtonToFunMap(R.id.fun_sin); 441 addButtonToFunMap(R.id.fun_cos); 442 addButtonToFunMap(R.id.fun_tan); 443 addButtonToFunMap(R.id.fun_arcsin); 444 addButtonToFunMap(R.id.fun_arccos); 445 addButtonToFunMap(R.id.fun_arctan); 446 addButtonToFunMap(R.id.fun_ln); 447 addButtonToFunMap(R.id.fun_log); 448 449 // Set locale-dependent character "constants" 450 mDecimalPt = 451 DecimalFormatSymbols.getInstance().getDecimalSeparator(); 452 // We recognize this in keyboard input, even if we use 453 // a different character. 454 Resources res = mActivity.getResources(); 455 mPiChar = 0; 456 String piString = res.getString(R.string.const_pi); 457 if (piString.length() == 1) { 458 mPiChar = piString.charAt(0); 459 } 460 461 sOutputForResultChar = new HashMap<Character, String>(); 462 sOutputForResultChar.put('e', "E"); 463 sOutputForResultChar.put('E', "E"); 464 sOutputForResultChar.put(' ', String.valueOf(CHAR_DIGIT_UNKNOWN)); 465 sOutputForResultChar.put(ELLIPSIS.charAt(0), ELLIPSIS); 466 sOutputForResultChar.put('/', "/"); 467 // Translate numbers for fraction display, but not 468 // the separating slash, which appears to be 469 // universal. 470 addButtonToOutputMap('-', R.id.op_sub); 471 addButtonToOutputMap('.', R.id.dec_point); 472 for (int i = 0; i <= 9; ++i) { 473 addButtonToOutputMap((char)('0' + i), keyForDigVal(i)); 474 } 475 476 sLocaleForMaps = lname; 477 478 } 479 } 480 481 /** 482 * Return function button id for the substring of s starting at pos and ending with 483 * the next "(". Return NO_ID if there is none. 484 * We currently check for both (possibly localized) button labels, and standard 485 * English names. (They should currently be the same, and hence this is currently redundant.) 486 * Callable only from UI thread. 487 */ 488 public static int funForString(String s, int pos) { 489 validateMaps(); 490 int parenPos = s.indexOf('(', pos); 491 if (parenPos != -1) { 492 String funString = s.substring(pos, parenPos); 493 Integer keyValue = sKeyValForFun.get(funString); 494 if (keyValue == null) return View.NO_ID; 495 return keyValue; 496 } 497 return View.NO_ID; 498 } 499 500 /** 501 * Return the localization of the string s representing a numeric answer. 502 * Callable only from UI thread. 503 */ 504 public static String translateResult(String s) { 505 StringBuilder result = new StringBuilder(); 506 int len = s.length(); 507 validateMaps(); 508 for (int i = 0; i < len; ++i) { 509 char c = s.charAt(i); 510 String translation = sOutputForResultChar.get(c); 511 if (translation == null) { 512 // Should not get here. Report if we do. 513 Log.v("Calculator", "Bad character:" + c); 514 result.append(String.valueOf(c)); 515 } else { 516 result.append(translation); 517 } 518 } 519 return result.toString(); 520 } 521 522 } 523