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