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