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