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