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