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