1 /* 2 * Copyright (C) 2010 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 package com.android.voicedialer; 17 18 19 import android.content.ContentUris; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.pm.PackageManager; 23 import android.content.pm.ResolveInfo; 24 import android.content.res.Resources; 25 import android.net.Uri; 26 import android.provider.ContactsContract.CommonDataKinds.Phone; 27 import android.speech.srec.Recognizer; 28 import android.util.Log; 29 30 import java.io.File; 31 import java.io.FileFilter; 32 import java.io.FileInputStream; 33 import java.io.FileOutputStream; 34 import java.io.IOException; 35 import java.io.ObjectInputStream; 36 import java.io.ObjectOutputStream; 37 import java.net.URISyntaxException; 38 import java.util.ArrayList; 39 import java.util.HashMap; 40 import java.util.HashSet; 41 import java.util.List; 42 /** 43 * This is a RecognizerEngine that processes commands to make phone calls and 44 * open applications. 45 * <ul> 46 * <li>setupGrammar 47 * <li>Scans contacts and determine if the Grammar g2g file is stale. 48 * <li>If so, create and rebuild the Grammar, 49 * <li>Else create and load the Grammar from the file. 50 * <li>onRecognitionSuccess is called when we get results from the recognizer, 51 * it will process the results, which will pass a list of intents to 52 * the {@RecognizerClient}. It will accept the following types of commands: 53 * "call" a particular contact 54 * "dial a particular number 55 * "open" a particular application 56 * "redial" the last number called 57 * "voicemail" to call voicemail 58 * <li>Pass a list of {@link Intent} corresponding to the recognition results 59 * to the {@link RecognizerClient}, which notifies the user. 60 * </ul> 61 * Notes: 62 * <ul> 63 * <li>Audio many be read from a file. 64 * <li>A directory tree of audio files may be stepped through. 65 * <li>A contact list may be read from a file. 66 * <li>A {@link RecognizerLogger} may generate a set of log files from 67 * a recognition session. 68 * <li>A static instance of this class is held and reused by the 69 * {@link VoiceDialerActivity}, which saves setup time. 70 * </ul> 71 */ 72 public class CommandRecognizerEngine extends RecognizerEngine { 73 74 private static final String OPEN_ENTRIES = "openentries.txt"; 75 public static final String PHONE_TYPE_EXTRA = "phone_type"; 76 private static final int MINIMUM_CONFIDENCE = 100; 77 private File mContactsFile; 78 private boolean mMinimizeResults; 79 private boolean mAllowOpenEntries; 80 private HashMap<String,String> mOpenEntries; 81 82 /** 83 * Constructor. 84 */ 85 public CommandRecognizerEngine() { 86 mContactsFile = null; 87 mMinimizeResults = false; 88 mAllowOpenEntries = true; 89 } 90 91 public void setContactsFile(File contactsFile) { 92 if (contactsFile != mContactsFile) { 93 mContactsFile = contactsFile; 94 // if we change the contacts file, then we need to recreate the grammar. 95 if (mSrecGrammar != null) { 96 mSrecGrammar.destroy(); 97 mSrecGrammar = null; 98 mOpenEntries = null; 99 } 100 } 101 } 102 103 public void setMinimizeResults(boolean minimizeResults) { 104 mMinimizeResults = minimizeResults; 105 } 106 107 public void setAllowOpenEntries(boolean allowOpenEntries) { 108 if (mAllowOpenEntries != allowOpenEntries) { 109 // if we change this setting, then we need to recreate the grammar. 110 if (mSrecGrammar != null) { 111 mSrecGrammar.destroy(); 112 mSrecGrammar = null; 113 mOpenEntries = null; 114 } 115 } 116 mAllowOpenEntries = allowOpenEntries; 117 } 118 119 protected void setupGrammar() throws IOException, InterruptedException { 120 // fetch the contact list 121 if (false) Log.d(TAG, "start getVoiceContacts"); 122 if (false) Log.d(TAG, "contactsFile is " + (mContactsFile == null ? 123 "null" : "not null")); 124 List<VoiceContact> contacts = mContactsFile != null ? 125 VoiceContact.getVoiceContactsFromFile(mContactsFile) : 126 VoiceContact.getVoiceContacts(mActivity); 127 128 // log contacts if requested 129 if (mLogger != null) mLogger.logContacts(contacts); 130 // generate g2g grammar file name 131 File g2g = mActivity.getFileStreamPath("voicedialer." + 132 Integer.toHexString(contacts.hashCode()) + ".g2g"); 133 134 // rebuild g2g file if current one is out of date 135 if (!g2g.exists()) { 136 // clean up existing Grammar and old file 137 deleteAllG2GFiles(mActivity); 138 if (mSrecGrammar != null) { 139 mSrecGrammar.destroy(); 140 mSrecGrammar = null; 141 } 142 143 // load the empty Grammar 144 if (false) Log.d(TAG, "start new Grammar"); 145 mSrecGrammar = mSrec.new Grammar(SREC_DIR + "/grammars/VoiceDialer.g2g"); 146 mSrecGrammar.setupRecognizer(); 147 148 // reset slots 149 if (false) Log.d(TAG, "start grammar.resetAllSlots"); 150 mSrecGrammar.resetAllSlots(); 151 152 // add names to the grammar 153 addNameEntriesToGrammar(contacts); 154 155 if (mAllowOpenEntries) { 156 // add open entries to the grammar 157 addOpenEntriesToGrammar(); 158 } 159 160 // compile the grammar 161 if (false) Log.d(TAG, "start grammar.compile"); 162 mSrecGrammar.compile(); 163 164 // update g2g file 165 if (false) Log.d(TAG, "start grammar.save " + g2g.getPath()); 166 g2g.getParentFile().mkdirs(); 167 mSrecGrammar.save(g2g.getPath()); 168 } 169 170 // g2g file exists, but is not loaded 171 else if (mSrecGrammar == null) { 172 if (false) Log.d(TAG, "start new Grammar loading " + g2g); 173 mSrecGrammar = mSrec.new Grammar(g2g.getPath()); 174 mSrecGrammar.setupRecognizer(); 175 } 176 if (mOpenEntries == null && mAllowOpenEntries) { 177 // make sure to load the openEntries mapping table. 178 loadOpenEntriesTable(); 179 } 180 181 } 182 183 /** 184 * Number of phone ids appended to a grammer in {@link #addNameEntriesToGrammar(List)}. 185 */ 186 private static final int PHONE_ID_COUNT = 7; 187 188 /** 189 * Add a list of names to the grammar 190 * @param contacts list of VoiceContacts to be added. 191 */ 192 private void addNameEntriesToGrammar(List<VoiceContact> contacts) 193 throws InterruptedException { 194 if (false) Log.d(TAG, "addNameEntriesToGrammar " + contacts.size()); 195 196 HashSet<String> entries = new HashSet<String>(); 197 StringBuilder sb = new StringBuilder(); 198 int count = 0; 199 for (VoiceContact contact : contacts) { 200 if (Thread.interrupted()) throw new InterruptedException(); 201 String name = scrubName(contact.mName); 202 if (name.length() == 0 || !entries.add(name)) continue; 203 sb.setLength(0); 204 // The number of ids appended here must be same as PHONE_ID_COUNT. 205 sb.append("V='"); 206 sb.append(contact.mContactId).append(' '); 207 sb.append(contact.mPrimaryId).append(' '); 208 sb.append(contact.mHomeId).append(' '); 209 sb.append(contact.mMobileId).append(' '); 210 sb.append(contact.mWorkId).append(' '); 211 sb.append(contact.mOtherId).append(' '); 212 sb.append(contact.mFallbackId); 213 sb.append("'"); 214 try { 215 mSrecGrammar.addWordToSlot("@Names", name, null, 1, sb.toString()); 216 } catch (Exception e) { 217 Log.e(TAG, "Cannot load all contacts to voice recognizer, loaded " + 218 count, e); 219 break; 220 } 221 222 count++; 223 } 224 } 225 226 /** 227 * add a list of application labels to the 'open x' grammar 228 */ 229 private void loadOpenEntriesTable() throws InterruptedException, IOException { 230 if (false) Log.d(TAG, "addOpenEntriesToGrammar"); 231 232 // fill this 233 File oe = mActivity.getFileStreamPath(OPEN_ENTRIES); 234 235 // build and write list of entries 236 if (!oe.exists()) { 237 mOpenEntries = new HashMap<String, String>(); 238 239 // build a list of 'open' entries 240 PackageManager pm = mActivity.getPackageManager(); 241 List<ResolveInfo> riList = pm.queryIntentActivities( 242 new Intent(Intent.ACTION_MAIN). 243 addCategory("android.intent.category.VOICE_LAUNCH"), 244 PackageManager.GET_ACTIVITIES); 245 if (Thread.interrupted()) throw new InterruptedException(); 246 riList.addAll(pm.queryIntentActivities( 247 new Intent(Intent.ACTION_MAIN). 248 addCategory("android.intent.category.LAUNCHER"), 249 PackageManager.GET_ACTIVITIES)); 250 String voiceDialerClassName = mActivity.getComponentName().getClassName(); 251 252 // scan list, adding complete phrases, as well as individual words 253 for (ResolveInfo ri : riList) { 254 if (Thread.interrupted()) throw new InterruptedException(); 255 256 // skip self 257 if (voiceDialerClassName.equals(ri.activityInfo.name)) continue; 258 259 // fetch a scrubbed window label 260 String label = scrubName(ri.loadLabel(pm).toString()); 261 if (label.length() == 0) continue; 262 263 // insert it into the result list 264 addClassName(mOpenEntries, label, 265 ri.activityInfo.packageName, ri.activityInfo.name); 266 267 // split it into individual words, and insert them 268 String[] words = label.split(" "); 269 if (words.length > 1) { 270 for (String word : words) { 271 word = word.trim(); 272 // words must be three characters long, or two if capitalized 273 int len = word.length(); 274 if (len <= 1) continue; 275 if (len == 2 && !(Character.isUpperCase(word.charAt(0)) && 276 Character.isUpperCase(word.charAt(1)))) continue; 277 if ("and".equalsIgnoreCase(word) || 278 "the".equalsIgnoreCase(word)) continue; 279 // add the word 280 addClassName(mOpenEntries, word, 281 ri.activityInfo.packageName, ri.activityInfo.name); 282 } 283 } 284 } 285 286 // write list 287 if (false) Log.d(TAG, "addOpenEntriesToGrammar writing " + oe); 288 try { 289 FileOutputStream fos = new FileOutputStream(oe); 290 try { 291 ObjectOutputStream oos = new ObjectOutputStream(fos); 292 oos.writeObject(mOpenEntries); 293 oos.close(); 294 } finally { 295 fos.close(); 296 } 297 } catch (IOException ioe) { 298 deleteCachedGrammarFiles(mActivity); 299 throw ioe; 300 } 301 } 302 303 // read the list 304 else { 305 if (false) Log.d(TAG, "addOpenEntriesToGrammar reading " + oe); 306 try { 307 FileInputStream fis = new FileInputStream(oe); 308 try { 309 ObjectInputStream ois = new ObjectInputStream(fis); 310 mOpenEntries = (HashMap<String, String>)ois.readObject(); 311 ois.close(); 312 } finally { 313 fis.close(); 314 } 315 } catch (Exception e) { 316 deleteCachedGrammarFiles(mActivity); 317 throw new IOException(e.toString()); 318 } 319 } 320 } 321 322 private void addOpenEntriesToGrammar() throws InterruptedException, IOException { 323 // load up our open entries table 324 loadOpenEntriesTable(); 325 326 // add list of 'open' entries to the grammar 327 for (String label : mOpenEntries.keySet()) { 328 if (Thread.interrupted()) throw new InterruptedException(); 329 String entry = mOpenEntries.get(label); 330 // don't add if too many results 331 int count = 0; 332 for (int i = 0; 0 != (i = entry.indexOf(' ', i) + 1); count++) ; 333 if (count > RESULT_LIMIT) continue; 334 // add the word to the grammar 335 // See Bug: 2457238. 336 // We used to store the entire list of components into the grammar. 337 // Unfortuantely, the recognizer has a fixed limit on the length of 338 // the "semantic" string, which is easy to overflow. So now, 339 // the we store our own mapping table between words and component 340 // names, and the entries in the grammar have the same value 341 // for literal and semantic. 342 mSrecGrammar.addWordToSlot("@Opens", label, null, 1, "V='" + label + "'"); 343 } 344 } 345 346 /** 347 * Add a className to a hash table of class name lists. 348 * @param openEntries HashMap of lists of class names. 349 * @param label a label or word corresponding to the list of classes. 350 * @param className class name to add 351 */ 352 private static void addClassName(HashMap<String,String> openEntries, 353 String label, String packageName, String className) { 354 String component = packageName + "/" + className; 355 String labelLowerCase = label.toLowerCase(); 356 String classList = openEntries.get(labelLowerCase); 357 358 // first item in the list 359 if (classList == null) { 360 openEntries.put(labelLowerCase, component); 361 return; 362 } 363 // already in list 364 int index = classList.indexOf(component); 365 int after = index + component.length(); 366 if (index != -1 && (index == 0 || classList.charAt(index - 1) == ' ') && 367 (after == classList.length() || classList.charAt(after) == ' ')) return; 368 369 // add it to the end 370 openEntries.put(labelLowerCase, classList + ' ' + component); 371 } 372 373 // map letters in Latin1 Supplement to basic ascii 374 // from http://en.wikipedia.org/wiki/Latin-1_Supplement_unicode_block 375 // not all letters map well, including Eth and Thorn 376 // TODO: this should really be all handled in the pronunciation engine 377 private final static char[] mLatin1Letters = 378 "AAAAAAACEEEEIIIIDNOOOOO OUUUUYDsaaaaaaaceeeeiiiidnooooo ouuuuydy". 379 toCharArray(); 380 private final static int mLatin1Base = 0x00c0; 381 382 /** 383 * Reformat a raw name from the contact list into a form a 384 * {@link Recognizer.Grammar} can digest. 385 * @param name the raw name. 386 * @return the reformatted name. 387 */ 388 private static String scrubName(String name) { 389 // replace '&' with ' and ' 390 name = name.replace("&", " and "); 391 392 // replace '@' with ' at ' 393 name = name.replace("@", " at "); 394 395 // remove '(...)' 396 while (true) { 397 int i = name.indexOf('('); 398 if (i == -1) break; 399 int j = name.indexOf(')', i); 400 if (j == -1) break; 401 name = name.substring(0, i) + " " + name.substring(j + 1); 402 } 403 404 // map letters of Latin1 Supplement to basic ascii 405 char[] nm = null; 406 for (int i = name.length() - 1; i >= 0; i--) { 407 char ch = name.charAt(i); 408 if (ch < ' ' || '~' < ch) { 409 if (nm == null) nm = name.toCharArray(); 410 nm[i] = mLatin1Base <= ch && ch < mLatin1Base + mLatin1Letters.length ? 411 mLatin1Letters[ch - mLatin1Base] : ' '; 412 } 413 } 414 if (nm != null) { 415 name = new String(nm); 416 } 417 418 // if '.' followed by alnum, replace with ' dot ' 419 while (true) { 420 int i = name.indexOf('.'); 421 if (i == -1 || 422 i + 1 >= name.length() || 423 !Character.isLetterOrDigit(name.charAt(i + 1))) break; 424 name = name.substring(0, i) + " dot " + name.substring(i + 1); 425 } 426 427 // trim 428 name = name.trim(); 429 430 // ensure at least one alphanumeric character, or the pron engine will fail 431 for (int i = name.length() - 1; true; i--) { 432 if (i < 0) return ""; 433 char ch = name.charAt(i); 434 if (('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || ('0' <= ch && ch <= '9')) { 435 break; 436 } 437 } 438 439 return name; 440 } 441 442 /** 443 * Delete all g2g files in the directory indicated by {@link File}, 444 * which is typically /data/data/com.android.voicedialer/files. 445 * There should only be one g2g file at any one time, with a hashcode 446 * embedded in it's name, but if stale ones are present, this will delete 447 * them all. 448 * @param context fetch directory for the stuffed and compiled g2g file. 449 */ 450 private static void deleteAllG2GFiles(Context context) { 451 FileFilter ff = new FileFilter() { 452 public boolean accept(File f) { 453 String name = f.getName(); 454 return name.endsWith(".g2g"); 455 } 456 }; 457 File[] files = context.getFilesDir().listFiles(ff); 458 if (files != null) { 459 for (File file : files) { 460 if (false) Log.d(TAG, "deleteAllG2GFiles " + file); 461 file.delete(); 462 } 463 } 464 } 465 466 /** 467 * Delete G2G and OpenEntries files, to force regeneration of the g2g file 468 * from scratch. 469 * @param context fetch directory for file. 470 */ 471 public static void deleteCachedGrammarFiles(Context context) { 472 deleteAllG2GFiles(context); 473 File oe = context.getFileStreamPath(OPEN_ENTRIES); 474 if (false) Log.v(TAG, "deleteCachedGrammarFiles " + oe); 475 if (oe.exists()) oe.delete(); 476 } 477 478 // NANP number formats 479 private final static String mNanpFormats = 480 "xxx xxx xxxx\n" + 481 "xxx xxxx\n" + 482 "x11\n"; 483 484 // a list of country codes 485 private final static String mPlusFormats = 486 487 //////////////////////////////////////////////////////////// 488 // zone 1: nanp (north american numbering plan), us, canada, caribbean 489 //////////////////////////////////////////////////////////// 490 491 "+1 xxx xxx xxxx\n" + // nanp 492 493 //////////////////////////////////////////////////////////// 494 // zone 2: africa, some atlantic and indian ocean islands 495 //////////////////////////////////////////////////////////// 496 497 "+20 x xxx xxxx\n" + // Egypt 498 "+20 1x xxx xxxx\n" + // Egypt 499 "+20 xx xxx xxxx\n" + // Egypt 500 "+20 xxx xxx xxxx\n" + // Egypt 501 502 "+212 xxxx xxxx\n" + // Morocco 503 504 "+213 xx xx xx xx\n" + // Algeria 505 "+213 xx xxx xxxx\n" + // Algeria 506 507 "+216 xx xxx xxx\n" + // Tunisia 508 509 "+218 xx xxx xxx\n" + // Libya 510 511 "+22x \n" + 512 "+23x \n" + 513 "+24x \n" + 514 "+25x \n" + 515 "+26x \n" + 516 517 "+27 xx xxx xxxx\n" + // South africa 518 519 "+290 x xxx\n" + // Saint Helena, Tristan da Cunha 520 521 "+291 x xxx xxx\n" + // Eritrea 522 523 "+297 xxx xxxx\n" + // Aruba 524 525 "+298 xxx xxx\n" + // Faroe Islands 526 527 "+299 xxx xxx\n" + // Greenland 528 529 //////////////////////////////////////////////////////////// 530 // zone 3: europe, southern and small countries 531 //////////////////////////////////////////////////////////// 532 533 "+30 xxx xxx xxxx\n" + // Greece 534 535 "+31 6 xxxx xxxx\n" + // Netherlands 536 "+31 xx xxx xxxx\n" + // Netherlands 537 "+31 xxx xx xxxx\n" + // Netherlands 538 539 "+32 2 xxx xx xx\n" + // Belgium 540 "+32 3 xxx xx xx\n" + // Belgium 541 "+32 4xx xx xx xx\n" + // Belgium 542 "+32 9 xxx xx xx\n" + // Belgium 543 "+32 xx xx xx xx\n" + // Belgium 544 545 "+33 xxx xxx xxx\n" + // France 546 547 "+34 xxx xxx xxx\n" + // Spain 548 549 "+351 3xx xxx xxx\n" + // Portugal 550 "+351 7xx xxx xxx\n" + // Portugal 551 "+351 8xx xxx xxx\n" + // Portugal 552 "+351 xx xxx xxxx\n" + // Portugal 553 554 "+352 xx xxxx\n" + // Luxembourg 555 "+352 6x1 xxx xxx\n" + // Luxembourg 556 "+352 \n" + // Luxembourg 557 558 "+353 xxx xxxx\n" + // Ireland 559 "+353 xxxx xxxx\n" + // Ireland 560 "+353 xx xxx xxxx\n" + // Ireland 561 562 "+354 3xx xxx xxx\n" + // Iceland 563 "+354 xxx xxxx\n" + // Iceland 564 565 "+355 6x xxx xxxx\n" + // Albania 566 "+355 xxx xxxx\n" + // Albania 567 568 "+356 xx xx xx xx\n" + // Malta 569 570 "+357 xx xx xx xx\n" + // Cyprus 571 572 "+358 \n" + // Finland 573 574 "+359 \n" + // Bulgaria 575 576 "+36 1 xxx xxxx\n" + // Hungary 577 "+36 20 xxx xxxx\n" + // Hungary 578 "+36 21 xxx xxxx\n" + // Hungary 579 "+36 30 xxx xxxx\n" + // Hungary 580 "+36 70 xxx xxxx\n" + // Hungary 581 "+36 71 xxx xxxx\n" + // Hungary 582 "+36 xx xxx xxx\n" + // Hungary 583 584 "+370 6x xxx xxx\n" + // Lithuania 585 "+370 xxx xx xxx\n" + // Lithuania 586 587 "+371 xxxx xxxx\n" + // Latvia 588 589 "+372 5 xxx xxxx\n" + // Estonia 590 "+372 xxx xxxx\n" + // Estonia 591 592 "+373 6xx xx xxx\n" + // Moldova 593 "+373 7xx xx xxx\n" + // Moldova 594 "+373 xxx xxxxx\n" + // Moldova 595 596 "+374 xx xxx xxx\n" + // Armenia 597 598 "+375 xx xxx xxxx\n" + // Belarus 599 600 "+376 xx xx xx\n" + // Andorra 601 602 "+377 xxxx xxxx\n" + // Monaco 603 604 "+378 xxx xxx xxxx\n" + // San Marino 605 606 "+380 xxx xx xx xx\n" + // Ukraine 607 608 "+381 xx xxx xxxx\n" + // Serbia 609 610 "+382 xx xxx xxxx\n" + // Montenegro 611 612 "+385 xx xxx xxxx\n" + // Croatia 613 614 "+386 x xxx xxxx\n" + // Slovenia 615 616 "+387 xx xx xx xx\n" + // Bosnia and herzegovina 617 618 "+389 2 xxx xx xx\n" + // Macedonia 619 "+389 xx xx xx xx\n" + // Macedonia 620 621 "+39 xxx xxx xxx\n" + // Italy 622 "+39 3xx xxx xxxx\n" + // Italy 623 "+39 xx xxxx xxxx\n" + // Italy 624 625 //////////////////////////////////////////////////////////// 626 // zone 4: europe, northern countries 627 //////////////////////////////////////////////////////////// 628 629 "+40 xxx xxx xxx\n" + // Romania 630 631 "+41 xx xxx xx xx\n" + // Switzerland 632 633 "+420 xxx xxx xxx\n" + // Czech republic 634 635 "+421 xxx xxx xxx\n" + // Slovakia 636 637 "+421 xxx xxx xxxx\n" + // Liechtenstein 638 639 "+43 \n" + // Austria 640 641 "+44 xxx xxx xxxx\n" + // UK 642 643 "+45 xx xx xx xx\n" + // Denmark 644 645 "+46 \n" + // Sweden 646 647 "+47 xxxx xxxx\n" + // Norway 648 649 "+48 xx xxx xxxx\n" + // Poland 650 651 "+49 1xx xxxx xxx\n" + // Germany 652 "+49 1xx xxxx xxxx\n" + // Germany 653 "+49 \n" + // Germany 654 655 //////////////////////////////////////////////////////////// 656 // zone 5: latin america 657 //////////////////////////////////////////////////////////// 658 659 "+50x \n" + 660 661 "+51 9xx xxx xxx\n" + // Peru 662 "+51 1 xxx xxxx\n" + // Peru 663 "+51 xx xx xxxx\n" + // Peru 664 665 "+52 1 xxx xxx xxxx\n" + // Mexico 666 "+52 xxx xxx xxxx\n" + // Mexico 667 668 "+53 xxxx xxxx\n" + // Cuba 669 670 "+54 9 11 xxxx xxxx\n" + // Argentina 671 "+54 9 xxx xxx xxxx\n" + // Argentina 672 "+54 11 xxxx xxxx\n" + // Argentina 673 "+54 xxx xxx xxxx\n" + // Argentina 674 675 "+55 xx xxxx xxxx\n" + // Brazil 676 677 "+56 2 xxxxxx\n" + // Chile 678 "+56 9 xxxx xxxx\n" + // Chile 679 "+56 xx xxxxxx\n" + // Chile 680 "+56 xx xxxxxxx\n" + // Chile 681 682 "+57 x xxx xxxx\n" + // Columbia 683 "+57 3xx xxx xxxx\n" + // Columbia 684 685 "+58 xxx xxx xxxx\n" + // Venezuela 686 687 "+59x \n" + 688 689 //////////////////////////////////////////////////////////// 690 // zone 6: southeast asia and oceania 691 //////////////////////////////////////////////////////////// 692 693 // TODO is this right? 694 "+60 3 xxxx xxxx\n" + // Malaysia 695 "+60 8x xxxxxx\n" + // Malaysia 696 "+60 x xxx xxxx\n" + // Malaysia 697 "+60 14 x xxx xxxx\n" + // Malaysia 698 "+60 1x xxx xxxx\n" + // Malaysia 699 "+60 x xxxx xxxx\n" + // Malaysia 700 "+60 \n" + // Malaysia 701 702 "+61 4xx xxx xxx\n" + // Australia 703 "+61 x xxxx xxxx\n" + // Australia 704 705 // TODO: is this right? 706 "+62 8xx xxxx xxxx\n" + // Indonesia 707 "+62 21 xxxxx\n" + // Indonesia 708 "+62 xx xxxxxx\n" + // Indonesia 709 "+62 xx xxx xxxx\n" + // Indonesia 710 "+62 xx xxxx xxxx\n" + // Indonesia 711 712 "+63 2 xxx xxxx\n" + // Phillipines 713 "+63 xx xxx xxxx\n" + // Phillipines 714 "+63 9xx xxx xxxx\n" + // Phillipines 715 716 // TODO: is this right? 717 "+64 2 xxx xxxx\n" + // New Zealand 718 "+64 2 xxx xxxx x\n" + // New Zealand 719 "+64 2 xxx xxxx xx\n" + // New Zealand 720 "+64 x xxx xxxx\n" + // New Zealand 721 722 "+65 xxxx xxxx\n" + // Singapore 723 724 "+66 8 xxxx xxxx\n" + // Thailand 725 "+66 2 xxx xxxx\n" + // Thailand 726 "+66 xx xx xxxx\n" + // Thailand 727 728 "+67x \n" + 729 "+68x \n" + 730 731 "+690 x xxx\n" + // Tokelau 732 733 "+691 xxx xxxx\n" + // Micronesia 734 735 "+692 xxx xxxx\n" + // marshall Islands 736 737 //////////////////////////////////////////////////////////// 738 // zone 7: russia and kazakstan 739 //////////////////////////////////////////////////////////// 740 741 "+7 6xx xx xxxxx\n" + // Kazakstan 742 "+7 7xx 2 xxxxxx\n" + // Kazakstan 743 "+7 7xx xx xxxxx\n" + // Kazakstan 744 745 "+7 xxx xxx xx xx\n" + // Russia 746 747 //////////////////////////////////////////////////////////// 748 // zone 8: east asia 749 //////////////////////////////////////////////////////////// 750 751 "+81 3 xxxx xxxx\n" + // Japan 752 "+81 6 xxxx xxxx\n" + // Japan 753 "+81 xx xxx xxxx\n" + // Japan 754 "+81 x0 xxxx xxxx\n" + // Japan 755 756 "+82 2 xxx xxxx\n" + // South korea 757 "+82 2 xxxx xxxx\n" + // South korea 758 "+82 xx xxxx xxxx\n" + // South korea 759 "+82 xx xxx xxxx\n" + // South korea 760 761 "+84 4 xxxx xxxx\n" + // Vietnam 762 "+84 xx xxxx xxx\n" + // Vietnam 763 "+84 xx xxxx xxxx\n" + // Vietnam 764 765 "+850 \n" + // North Korea 766 767 "+852 xxxx xxxx\n" + // Hong Kong 768 769 "+853 xxxx xxxx\n" + // Macau 770 771 "+855 1x xxx xxx\n" + // Cambodia 772 "+855 9x xxx xxx\n" + // Cambodia 773 "+855 xx xx xx xx\n" + // Cambodia 774 775 "+856 20 x xxx xxx\n" + // Laos 776 "+856 xx xxx xxx\n" + // Laos 777 778 "+852 xxxx xxxx\n" + // Hong kong 779 780 "+86 10 xxxx xxxx\n" + // China 781 "+86 2x xxxx xxxx\n" + // China 782 "+86 xxx xxx xxxx\n" + // China 783 "+86 xxx xxxx xxxx\n" + // China 784 785 "+880 xx xxxx xxxx\n" + // Bangladesh 786 787 "+886 \n" + // Taiwan 788 789 //////////////////////////////////////////////////////////// 790 // zone 9: south asia, west asia, central asia, middle east 791 //////////////////////////////////////////////////////////// 792 793 "+90 xxx xxx xxxx\n" + // Turkey 794 795 "+91 9x xx xxxxxx\n" + // India 796 "+91 xx xxxx xxxx\n" + // India 797 798 "+92 xx xxx xxxx\n" + // Pakistan 799 "+92 3xx xxx xxxx\n" + // Pakistan 800 801 "+93 70 xxx xxx\n" + // Afghanistan 802 "+93 xx xxx xxxx\n" + // Afghanistan 803 804 "+94 xx xxx xxxx\n" + // Sri Lanka 805 806 "+95 1 xxx xxx\n" + // Burma 807 "+95 2 xxx xxx\n" + // Burma 808 "+95 xx xxxxx\n" + // Burma 809 "+95 9 xxx xxxx\n" + // Burma 810 811 "+960 xxx xxxx\n" + // Maldives 812 813 "+961 x xxx xxx\n" + // Lebanon 814 "+961 xx xxx xxx\n" + // Lebanon 815 816 "+962 7 xxxx xxxx\n" + // Jordan 817 "+962 x xxx xxxx\n" + // Jordan 818 819 "+963 11 xxx xxxx\n" + // Syria 820 "+963 xx xxx xxx\n" + // Syria 821 822 "+964 \n" + // Iraq 823 824 "+965 xxxx xxxx\n" + // Kuwait 825 826 "+966 5x xxx xxxx\n" + // Saudi Arabia 827 "+966 x xxx xxxx\n" + // Saudi Arabia 828 829 "+967 7xx xxx xxx\n" + // Yemen 830 "+967 x xxx xxx\n" + // Yemen 831 832 "+968 xxxx xxxx\n" + // Oman 833 834 "+970 5x xxx xxxx\n" + // Palestinian Authority 835 "+970 x xxx xxxx\n" + // Palestinian Authority 836 837 "+971 5x xxx xxxx\n" + // United Arab Emirates 838 "+971 x xxx xxxx\n" + // United Arab Emirates 839 840 "+972 5x xxx xxxx\n" + // Israel 841 "+972 x xxx xxxx\n" + // Israel 842 843 "+973 xxxx xxxx\n" + // Bahrain 844 845 "+974 xxx xxxx\n" + // Qatar 846 847 "+975 1x xxx xxx\n" + // Bhutan 848 "+975 x xxx xxx\n" + // Bhutan 849 850 "+976 \n" + // Mongolia 851 852 "+977 xxxx xxxx\n" + // Nepal 853 "+977 98 xxxx xxxx\n" + // Nepal 854 855 "+98 xxx xxx xxxx\n" + // Iran 856 857 "+992 xxx xxx xxx\n" + // Tajikistan 858 859 "+993 xxxx xxxx\n" + // Turkmenistan 860 861 "+994 xx xxx xxxx\n" + // Azerbaijan 862 "+994 xxx xxxxx\n" + // Azerbaijan 863 864 "+995 xx xxx xxx\n" + // Georgia 865 866 "+996 xxx xxx xxx\n" + // Kyrgyzstan 867 868 "+998 xx xxx xxxx\n"; // Uzbekistan 869 870 871 // TODO: need to handle variable number notation 872 private static String formatNumber(String formats, String number) { 873 number = number.trim(); 874 final int nlen = number.length(); 875 final int formatslen = formats.length(); 876 StringBuffer sb = new StringBuffer(); 877 878 // loop over country codes 879 for (int f = 0; f < formatslen; ) { 880 sb.setLength(0); 881 int n = 0; 882 883 // loop over letters of pattern 884 while (true) { 885 final char fch = formats.charAt(f); 886 if (fch == '\n' && n >= nlen) return sb.toString(); 887 if (fch == '\n' || n >= nlen) break; 888 final char nch = number.charAt(n); 889 // pattern matches number 890 if (fch == nch || (fch == 'x' && Character.isDigit(nch))) { 891 f++; 892 n++; 893 sb.append(nch); 894 } 895 // don't match ' ' in pattern, but insert into result 896 else if (fch == ' ') { 897 f++; 898 sb.append(' '); 899 // ' ' at end -> match all the rest 900 if (formats.charAt(f) == '\n') { 901 return sb.append(number, n, nlen).toString(); 902 } 903 } 904 // match failed 905 else break; 906 } 907 908 // step to the next pattern 909 f = formats.indexOf('\n', f) + 1; 910 if (f == 0) break; 911 } 912 913 return null; 914 } 915 916 /** 917 * Format a phone number string. 918 * At some point, PhoneNumberUtils.formatNumber will handle this. 919 * @param num phone number string. 920 * @return formatted phone number string. 921 */ 922 private static String formatNumber(String num) { 923 String fmt = null; 924 925 fmt = formatNumber(mPlusFormats, num); 926 if (fmt != null) return fmt; 927 928 fmt = formatNumber(mNanpFormats, num); 929 if (fmt != null) return fmt; 930 931 return null; 932 } 933 934 /** 935 * Called when recognition succeeds. It receives a list 936 * of results, builds a corresponding list of Intents, and 937 * passes them to the {@link RecognizerClient}, which selects and 938 * performs a corresponding action. 939 * @param recognizerClient the client that will be sent the results 940 */ 941 protected void onRecognitionSuccess(RecognizerClient recognizerClient) 942 throws InterruptedException { 943 if (false) Log.d(TAG, "onRecognitionSuccess"); 944 945 if (mLogger != null) mLogger.logNbestHeader(); 946 947 ArrayList<Intent> intents = new ArrayList<Intent>(); 948 949 int highestConfidence = 0; 950 int examineLimit = RESULT_LIMIT; 951 if (mMinimizeResults) { 952 examineLimit = 1; 953 } 954 for (int result = 0; result < mSrec.getResultCount() && 955 intents.size() < examineLimit; result++) { 956 957 // parse the semanticMeaning string and build an Intent 958 String conf = mSrec.getResult(result, Recognizer.KEY_CONFIDENCE); 959 String literal = mSrec.getResult(result, Recognizer.KEY_LITERAL); 960 String semantic = mSrec.getResult(result, Recognizer.KEY_MEANING); 961 String msg = "conf=" + conf + " lit=" + literal + " sem=" + semantic; 962 if (false) Log.d(TAG, msg); 963 int confInt = Integer.parseInt(conf); 964 if (highestConfidence < confInt) highestConfidence = confInt; 965 if (confInt < MINIMUM_CONFIDENCE || confInt * 2 < highestConfidence) { 966 if (false) Log.d(TAG, "confidence too low, dropping"); 967 break; 968 } 969 if (mLogger != null) mLogger.logLine(msg); 970 String[] commands = semantic.trim().split(" "); 971 972 // DIAL 650 867 5309 973 // DIAL 867 5309 974 // DIAL 911 975 if ("DIAL".equalsIgnoreCase(commands[0])) { 976 Uri uri = Uri.fromParts("tel", commands[1], null); 977 String num = formatNumber(commands[1]); 978 if (num != null) { 979 addCallIntent(intents, uri, 980 literal.split(" ")[0].trim() + " " + num, "", 0); 981 } 982 } 983 984 // CALL JACK JONES 985 // commands should become ["CALL", id, id, ..] reflecting addNameEntriesToGrammar(). 986 else if ("CALL".equalsIgnoreCase(commands[0]) 987 && commands.length >= PHONE_ID_COUNT + 1) { 988 // parse the ids 989 long contactId = Long.parseLong(commands[1]); // people table 990 long primaryId = Long.parseLong(commands[2]); // phones table 991 long homeId = Long.parseLong(commands[3]); // phones table 992 long mobileId = Long.parseLong(commands[4]); // phones table 993 long workId = Long.parseLong(commands[5]); // phones table 994 long otherId = Long.parseLong(commands[6]); // phones table 995 long fallbackId = Long.parseLong(commands[7]); // phones table 996 Resources res = mActivity.getResources(); 997 998 int count = 0; 999 1000 // 1001 // generate the best entry corresponding to what was said 1002 // 1003 1004 // 'CALL JACK JONES AT HOME|MOBILE|WORK|OTHER' 1005 if (commands.length == PHONE_ID_COUNT + 2) { 1006 // The last command should imply the type of the phone number. 1007 final String spokenPhoneIdCommand = commands[PHONE_ID_COUNT + 1]; 1008 long spokenPhoneId = 1009 "H".equalsIgnoreCase(spokenPhoneIdCommand) ? homeId : 1010 "M".equalsIgnoreCase(spokenPhoneIdCommand) ? mobileId : 1011 "W".equalsIgnoreCase(spokenPhoneIdCommand) ? workId : 1012 "O".equalsIgnoreCase(spokenPhoneIdCommand) ? otherId : 1013 VoiceContact.ID_UNDEFINED; 1014 if (spokenPhoneId != VoiceContact.ID_UNDEFINED) { 1015 addCallIntent(intents, ContentUris.withAppendedId( 1016 Phone.CONTENT_URI, spokenPhoneId), 1017 literal, spokenPhoneIdCommand, 0); 1018 count++; 1019 } 1020 } 1021 1022 // 'CALL JACK JONES', with valid default phoneId 1023 else if (commands.length == PHONE_ID_COUNT + 1) { 1024 String phoneType = null; 1025 CharSequence phoneIdMsg = null; 1026 if (primaryId == VoiceContact.ID_UNDEFINED) { 1027 phoneType = null; 1028 phoneIdMsg = null; 1029 } else if (primaryId == homeId) { 1030 phoneType = "H"; 1031 phoneIdMsg = res.getText(R.string.at_home); 1032 } else if (primaryId == mobileId) { 1033 phoneType = "M"; 1034 phoneIdMsg = res.getText(R.string.on_mobile); 1035 } else if (primaryId == workId) { 1036 phoneType = "W"; 1037 phoneIdMsg = res.getText(R.string.at_work); 1038 } else if (primaryId == otherId) { 1039 phoneType = "O"; 1040 phoneIdMsg = res.getText(R.string.at_other); 1041 } 1042 if (phoneIdMsg != null) { 1043 addCallIntent(intents, ContentUris.withAppendedId( 1044 Phone.CONTENT_URI, primaryId), 1045 literal + phoneIdMsg, phoneType, 0); 1046 count++; 1047 } 1048 } 1049 1050 if (count == 0 || !mMinimizeResults) { 1051 // 1052 // generate all other entries for this person 1053 // 1054 1055 // trim last two words, ie 'at home', etc 1056 String lit = literal; 1057 if (commands.length == PHONE_ID_COUNT + 2) { 1058 String[] words = literal.trim().split(" "); 1059 StringBuffer sb = new StringBuffer(); 1060 for (int i = 0; i < words.length - 2; i++) { 1061 if (i != 0) { 1062 sb.append(' '); 1063 } 1064 sb.append(words[i]); 1065 } 1066 lit = sb.toString(); 1067 } 1068 1069 // add 'CALL JACK JONES at home' using phoneId 1070 if (homeId != VoiceContact.ID_UNDEFINED) { 1071 addCallIntent(intents, ContentUris.withAppendedId( 1072 Phone.CONTENT_URI, homeId), 1073 lit + res.getText(R.string.at_home), "H", 0); 1074 count++; 1075 } 1076 1077 // add 'CALL JACK JONES on mobile' using mobileId 1078 if (mobileId != VoiceContact.ID_UNDEFINED) { 1079 addCallIntent(intents, ContentUris.withAppendedId( 1080 Phone.CONTENT_URI, mobileId), 1081 lit + res.getText(R.string.on_mobile), "M", 0); 1082 count++; 1083 } 1084 1085 // add 'CALL JACK JONES at work' using workId 1086 if (workId != VoiceContact.ID_UNDEFINED) { 1087 addCallIntent(intents, ContentUris.withAppendedId( 1088 Phone.CONTENT_URI, workId), 1089 lit + res.getText(R.string.at_work), "W", 0); 1090 count++; 1091 } 1092 1093 // add 'CALL JACK JONES at other' using otherId 1094 if (otherId != VoiceContact.ID_UNDEFINED) { 1095 addCallIntent(intents, ContentUris.withAppendedId( 1096 Phone.CONTENT_URI, otherId), 1097 lit + res.getText(R.string.at_other), "O", 0); 1098 count++; 1099 } 1100 1101 if (fallbackId != VoiceContact.ID_UNDEFINED) { 1102 addCallIntent(intents, ContentUris.withAppendedId( 1103 Phone.CONTENT_URI, fallbackId), 1104 lit, "", 0); 1105 count++; 1106 } 1107 } 1108 } 1109 1110 else if ("X".equalsIgnoreCase(commands[0])) { 1111 Intent intent = new Intent(RecognizerEngine.ACTION_RECOGNIZER_RESULT, null); 1112 intent.putExtra(RecognizerEngine.SENTENCE_EXTRA, literal); 1113 intent.putExtra(RecognizerEngine.SEMANTIC_EXTRA, semantic); 1114 addIntent(intents, intent); 1115 } 1116 1117 // "CALL VoiceMail" 1118 else if ("voicemail".equalsIgnoreCase(commands[0]) && commands.length == 1) { 1119 addCallIntent(intents, Uri.fromParts("voicemail", "x", null), 1120 literal, "", Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 1121 } 1122 1123 // "REDIAL" 1124 else if ("redial".equalsIgnoreCase(commands[0]) && commands.length == 1) { 1125 String number = VoiceContact.redialNumber(mActivity); 1126 if (number != null) { 1127 addCallIntent(intents, Uri.fromParts("tel", number, null), 1128 literal, "", Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 1129 } 1130 } 1131 1132 // "Intent ..." 1133 else if ("Intent".equalsIgnoreCase(commands[0])) { 1134 for (int i = 1; i < commands.length; i++) { 1135 try { 1136 Intent intent = Intent.getIntent(commands[i]); 1137 if (intent.getStringExtra(SENTENCE_EXTRA) == null) { 1138 intent.putExtra(SENTENCE_EXTRA, literal); 1139 } 1140 addIntent(intents, intent); 1141 } catch (URISyntaxException e) { 1142 if (false) { 1143 Log.d(TAG, "onRecognitionSuccess: poorly " + 1144 "formed URI in grammar" + e); 1145 } 1146 } 1147 } 1148 } 1149 1150 // "OPEN ..." 1151 else if ("OPEN".equalsIgnoreCase(commands[0]) && mAllowOpenEntries) { 1152 PackageManager pm = mActivity.getPackageManager(); 1153 if (commands.length > 1 & mOpenEntries != null) { 1154 // the semantic value is equal to the literal in this case. 1155 // We have to do the mapping from this text to the 1156 // componentname ourselves. See Bug: 2457238. 1157 // The problem is that the list of all componentnames 1158 // can be pretty large and overflow the limit that 1159 // the recognizer has. 1160 String meaning = mOpenEntries.get(commands[1]); 1161 String[] components = meaning.trim().split(" "); 1162 for (int i=0; i < components.length; i++) { 1163 String component = components[i]; 1164 Intent intent = new Intent(Intent.ACTION_MAIN); 1165 intent.addCategory("android.intent.category.VOICE_LAUNCH"); 1166 String packageName = component.substring( 1167 0, component.lastIndexOf('/')); 1168 String className = component.substring( 1169 component.lastIndexOf('/')+1, component.length()); 1170 intent.setClassName(packageName, className); 1171 List<ResolveInfo> riList = pm.queryIntentActivities(intent, 0); 1172 for (ResolveInfo ri : riList) { 1173 String label = ri.loadLabel(pm).toString(); 1174 intent = new Intent(Intent.ACTION_MAIN); 1175 intent.addCategory("android.intent.category.VOICE_LAUNCH"); 1176 intent.setClassName(packageName, className); 1177 intent.putExtra(SENTENCE_EXTRA, literal.split(" ")[0] + " " + label); 1178 addIntent(intents, intent); 1179 } 1180 } 1181 } 1182 } 1183 1184 // can't parse result 1185 else { 1186 if (false) Log.d(TAG, "onRecognitionSuccess: parse error"); 1187 } 1188 } 1189 1190 // log if requested 1191 if (mLogger != null) mLogger.logIntents(intents); 1192 1193 // bail out if cancelled 1194 if (Thread.interrupted()) throw new InterruptedException(); 1195 1196 if (intents.size() == 0) { 1197 // TODO: strip HOME|MOBILE|WORK and try default here? 1198 recognizerClient.onRecognitionFailure("No Intents generated"); 1199 } 1200 else { 1201 recognizerClient.onRecognitionSuccess( 1202 intents.toArray(new Intent[intents.size()])); 1203 } 1204 } 1205 1206 // only add if different 1207 private static void addCallIntent(ArrayList<Intent> intents, Uri uri, String literal, 1208 String phoneType, int flags) { 1209 Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, uri) 1210 .setFlags(flags) 1211 .putExtra(SENTENCE_EXTRA, literal) 1212 .putExtra(PHONE_TYPE_EXTRA, phoneType); 1213 addIntent(intents, intent); 1214 } 1215 } 1216