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