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