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