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