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