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