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