/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.voicedialer; import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.net.Uri; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.Contacts; import android.speech.srec.Recognizer; import android.util.Log; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; /** * This is a RecognizerEngine that processes commands to make phone calls and * open applications. * <ul> * <li>setupGrammar * <li>Scans contacts and determine if the Grammar g2g file is stale. * <li>If so, create and rebuild the Grammar, * <li>Else create and load the Grammar from the file. * <li>onRecognitionSuccess is called when we get results from the recognizer, * it will process the results, which will pass a list of intents to * the {@RecognizerClient}. It will accept the following types of commands: * "call" a particular contact * "dial a particular number * "open" a particular application * "redial" the last number called * "voicemail" to call voicemail * <li>Pass a list of {@link Intent} corresponding to the recognition results * to the {@link RecognizerClient}, which notifies the user. * </ul> * Notes: * <ul> * <li>Audio many be read from a file. * <li>A directory tree of audio files may be stepped through. * <li>A contact list may be read from a file. * <li>A {@link RecognizerLogger} may generate a set of log files from * a recognition session. * <li>A static instance of this class is held and reused by the * {@link VoiceDialerActivity}, which saves setup time. * </ul> */ public class CommandRecognizerEngine extends RecognizerEngine { private static final String OPEN_ENTRIES = "openentries.txt"; public static final String PHONE_TYPE_EXTRA = "phone_type"; private static final int MINIMUM_CONFIDENCE = 100; private File mContactsFile; private boolean mMinimizeResults; private boolean mAllowOpenEntries; private HashMap<String,String> mOpenEntries; /** * Constructor. */ public CommandRecognizerEngine() { mContactsFile = null; mMinimizeResults = false; mAllowOpenEntries = true; } public void setContactsFile(File contactsFile) { if (contactsFile != mContactsFile) { mContactsFile = contactsFile; // if we change the contacts file, then we need to recreate the grammar. if (mSrecGrammar != null) { mSrecGrammar.destroy(); mSrecGrammar = null; mOpenEntries = null; } } } public void setMinimizeResults(boolean minimizeResults) { mMinimizeResults = minimizeResults; } public void setAllowOpenEntries(boolean allowOpenEntries) { if (mAllowOpenEntries != allowOpenEntries) { // if we change this setting, then we need to recreate the grammar. if (mSrecGrammar != null) { mSrecGrammar.destroy(); mSrecGrammar = null; mOpenEntries = null; } } mAllowOpenEntries = allowOpenEntries; } protected void setupGrammar() throws IOException, InterruptedException { // fetch the contact list if (false) Log.d(TAG, "start getVoiceContacts"); if (false) Log.d(TAG, "contactsFile is " + (mContactsFile == null ? "null" : "not null")); List<VoiceContact> contacts = mContactsFile != null ? VoiceContact.getVoiceContactsFromFile(mContactsFile) : VoiceContact.getVoiceContacts(mActivity); // log contacts if requested if (mLogger != null) mLogger.logContacts(contacts); // generate g2g grammar file name File g2g = mActivity.getFileStreamPath("voicedialer." + Integer.toHexString(contacts.hashCode()) + ".g2g"); // rebuild g2g file if current one is out of date if (!g2g.exists()) { // clean up existing Grammar and old file deleteAllG2GFiles(mActivity); if (mSrecGrammar != null) { mSrecGrammar.destroy(); mSrecGrammar = null; } // load the empty Grammar if (false) Log.d(TAG, "start new Grammar"); mSrecGrammar = mSrec.new Grammar(SREC_DIR + "/grammars/VoiceDialer.g2g"); mSrecGrammar.setupRecognizer(); // reset slots if (false) Log.d(TAG, "start grammar.resetAllSlots"); mSrecGrammar.resetAllSlots(); // add names to the grammar addNameEntriesToGrammar(contacts); if (mAllowOpenEntries) { // add open entries to the grammar addOpenEntriesToGrammar(); } // compile the grammar if (false) Log.d(TAG, "start grammar.compile"); mSrecGrammar.compile(); // update g2g file if (false) Log.d(TAG, "start grammar.save " + g2g.getPath()); g2g.getParentFile().mkdirs(); mSrecGrammar.save(g2g.getPath()); } // g2g file exists, but is not loaded else if (mSrecGrammar == null) { if (false) Log.d(TAG, "start new Grammar loading " + g2g); mSrecGrammar = mSrec.new Grammar(g2g.getPath()); mSrecGrammar.setupRecognizer(); } if (mOpenEntries == null && mAllowOpenEntries) { // make sure to load the openEntries mapping table. loadOpenEntriesTable(); } } /** * Add a list of names to the grammar * @param contacts list of VoiceContacts to be added. */ private void addNameEntriesToGrammar(List<VoiceContact> contacts) throws InterruptedException { if (false) Log.d(TAG, "addNameEntriesToGrammar " + contacts.size()); HashSet<String> entries = new HashSet<String>(); StringBuffer sb = new StringBuffer(); int count = 0; for (VoiceContact contact : contacts) { if (Thread.interrupted()) throw new InterruptedException(); String name = scrubName(contact.mName); if (name.length() == 0 || !entries.add(name)) continue; sb.setLength(0); sb.append("V='"); sb.append(contact.mContactId).append(' '); sb.append(contact.mPrimaryId).append(' '); sb.append(contact.mHomeId).append(' '); sb.append(contact.mMobileId).append(' '); sb.append(contact.mWorkId).append(' '); sb.append(contact.mOtherId); sb.append("'"); try { mSrecGrammar.addWordToSlot("@Names", name, null, 1, sb.toString()); } catch (Exception e) { Log.e(TAG, "Cannot load all contacts to voice recognizer, loaded " + count, e); break; } count++; } } /** * add a list of application labels to the 'open x' grammar */ private void loadOpenEntriesTable() throws InterruptedException, IOException { if (false) Log.d(TAG, "addOpenEntriesToGrammar"); // fill this File oe = mActivity.getFileStreamPath(OPEN_ENTRIES); // build and write list of entries if (!oe.exists()) { mOpenEntries = new HashMap<String, String>(); // build a list of 'open' entries PackageManager pm = mActivity.getPackageManager(); List<ResolveInfo> riList = pm.queryIntentActivities( new Intent(Intent.ACTION_MAIN). addCategory("android.intent.category.VOICE_LAUNCH"), PackageManager.GET_ACTIVITIES); if (Thread.interrupted()) throw new InterruptedException(); riList.addAll(pm.queryIntentActivities( new Intent(Intent.ACTION_MAIN). addCategory("android.intent.category.LAUNCHER"), PackageManager.GET_ACTIVITIES)); String voiceDialerClassName = mActivity.getComponentName().getClassName(); // scan list, adding complete phrases, as well as individual words for (ResolveInfo ri : riList) { if (Thread.interrupted()) throw new InterruptedException(); // skip self if (voiceDialerClassName.equals(ri.activityInfo.name)) continue; // fetch a scrubbed window label String label = scrubName(ri.loadLabel(pm).toString()); if (label.length() == 0) continue; // insert it into the result list addClassName(mOpenEntries, label, ri.activityInfo.packageName, ri.activityInfo.name); // split it into individual words, and insert them String[] words = label.split(" "); if (words.length > 1) { for (String word : words) { word = word.trim(); // words must be three characters long, or two if capitalized int len = word.length(); if (len <= 1) continue; if (len == 2 && !(Character.isUpperCase(word.charAt(0)) && Character.isUpperCase(word.charAt(1)))) continue; if ("and".equalsIgnoreCase(word) || "the".equalsIgnoreCase(word)) continue; // add the word addClassName(mOpenEntries, word, ri.activityInfo.packageName, ri.activityInfo.name); } } } // write list if (false) Log.d(TAG, "addOpenEntriesToGrammar writing " + oe); try { FileOutputStream fos = new FileOutputStream(oe); try { ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(mOpenEntries); oos.close(); } finally { fos.close(); } } catch (IOException ioe) { deleteCachedGrammarFiles(mActivity); throw ioe; } } // read the list else { if (false) Log.d(TAG, "addOpenEntriesToGrammar reading " + oe); try { FileInputStream fis = new FileInputStream(oe); try { ObjectInputStream ois = new ObjectInputStream(fis); mOpenEntries = (HashMap<String, String>)ois.readObject(); ois.close(); } finally { fis.close(); } } catch (Exception e) { deleteCachedGrammarFiles(mActivity); throw new IOException(e.toString()); } } } private void addOpenEntriesToGrammar() throws InterruptedException, IOException { // load up our open entries table loadOpenEntriesTable(); // add list of 'open' entries to the grammar for (String label : mOpenEntries.keySet()) { if (Thread.interrupted()) throw new InterruptedException(); String entry = mOpenEntries.get(label); // don't add if too many results int count = 0; for (int i = 0; 0 != (i = entry.indexOf(' ', i) + 1); count++) ; if (count > RESULT_LIMIT) continue; // add the word to the grammar // See Bug: 2457238. // We used to store the entire list of components into the grammar. // Unfortuantely, the recognizer has a fixed limit on the length of // the "semantic" string, which is easy to overflow. So now, // the we store our own mapping table between words and component // names, and the entries in the grammar have the same value // for literal and semantic. mSrecGrammar.addWordToSlot("@Opens", label, null, 1, "V='" + label + "'"); } } /** * Add a className to a hash table of class name lists. * @param openEntries HashMap of lists of class names. * @param label a label or word corresponding to the list of classes. * @param className class name to add */ private static void addClassName(HashMap<String,String> openEntries, String label, String packageName, String className) { String component = packageName + "/" + className; String labelLowerCase = label.toLowerCase(); String classList = openEntries.get(labelLowerCase); // first item in the list if (classList == null) { openEntries.put(labelLowerCase, component); return; } // already in list int index = classList.indexOf(component); int after = index + component.length(); if (index != -1 && (index == 0 || classList.charAt(index - 1) == ' ') && (after == classList.length() || classList.charAt(after) == ' ')) return; // add it to the end openEntries.put(labelLowerCase, classList + ' ' + component); } // map letters in Latin1 Supplement to basic ascii // from http://en.wikipedia.org/wiki/Latin-1_Supplement_unicode_block // not all letters map well, including Eth and Thorn // TODO: this should really be all handled in the pronunciation engine private final static char[] mLatin1Letters = "AAAAAAACEEEEIIIIDNOOOOO OUUUUYDsaaaaaaaceeeeiiiidnooooo ouuuuydy". toCharArray(); private final static int mLatin1Base = 0x00c0; /** * Reformat a raw name from the contact list into a form a * {@link Recognizer.Grammar} can digest. * @param name the raw name. * @return the reformatted name. */ private static String scrubName(String name) { // replace '&' with ' and ' name = name.replace("&", " and "); // replace '@' with ' at ' name = name.replace("@", " at "); // remove '(...)' while (true) { int i = name.indexOf('('); if (i == -1) break; int j = name.indexOf(')', i); if (j == -1) break; name = name.substring(0, i) + " " + name.substring(j + 1); } // map letters of Latin1 Supplement to basic ascii char[] nm = null; for (int i = name.length() - 1; i >= 0; i--) { char ch = name.charAt(i); if (ch < ' ' || '~' < ch) { if (nm == null) nm = name.toCharArray(); nm[i] = mLatin1Base <= ch && ch < mLatin1Base + mLatin1Letters.length ? mLatin1Letters[ch - mLatin1Base] : ' '; } } if (nm != null) { name = new String(nm); } // if '.' followed by alnum, replace with ' dot ' while (true) { int i = name.indexOf('.'); if (i == -1 || i + 1 >= name.length() || !Character.isLetterOrDigit(name.charAt(i + 1))) break; name = name.substring(0, i) + " dot " + name.substring(i + 1); } // trim name = name.trim(); // ensure at least one alphanumeric character, or the pron engine will fail for (int i = name.length() - 1; true; i--) { if (i < 0) return ""; char ch = name.charAt(i); if (('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || ('0' <= ch && ch <= '9')) { break; } } return name; } /** * Delete all g2g files in the directory indicated by {@link File}, * which is typically /data/data/com.android.voicedialer/files. * There should only be one g2g file at any one time, with a hashcode * embedded in it's name, but if stale ones are present, this will delete * them all. * @param context fetch directory for the stuffed and compiled g2g file. */ private static void deleteAllG2GFiles(Context context) { FileFilter ff = new FileFilter() { public boolean accept(File f) { String name = f.getName(); return name.endsWith(".g2g"); } }; File[] files = context.getFilesDir().listFiles(ff); if (files != null) { for (File file : files) { if (false) Log.d(TAG, "deleteAllG2GFiles " + file); file.delete(); } } } /** * Delete G2G and OpenEntries files, to force regeneration of the g2g file * from scratch. * @param context fetch directory for file. */ public static void deleteCachedGrammarFiles(Context context) { deleteAllG2GFiles(context); File oe = context.getFileStreamPath(OPEN_ENTRIES); if (false) Log.v(TAG, "deleteCachedGrammarFiles " + oe); if (oe.exists()) oe.delete(); } // NANP number formats private final static String mNanpFormats = "xxx xxx xxxx\n" + "xxx xxxx\n" + "x11\n"; // a list of country codes private final static String mPlusFormats = //////////////////////////////////////////////////////////// // zone 1: nanp (north american numbering plan), us, canada, caribbean //////////////////////////////////////////////////////////// "+1 xxx xxx xxxx\n" + // nanp //////////////////////////////////////////////////////////// // zone 2: africa, some atlantic and indian ocean islands //////////////////////////////////////////////////////////// "+20 x xxx xxxx\n" + // Egypt "+20 1x xxx xxxx\n" + // Egypt "+20 xx xxx xxxx\n" + // Egypt "+20 xxx xxx xxxx\n" + // Egypt "+212 xxxx xxxx\n" + // Morocco "+213 xx xx xx xx\n" + // Algeria "+213 xx xxx xxxx\n" + // Algeria "+216 xx xxx xxx\n" + // Tunisia "+218 xx xxx xxx\n" + // Libya "+22x \n" + "+23x \n" + "+24x \n" + "+25x \n" + "+26x \n" + "+27 xx xxx xxxx\n" + // South africa "+290 x xxx\n" + // Saint Helena, Tristan da Cunha "+291 x xxx xxx\n" + // Eritrea "+297 xxx xxxx\n" + // Aruba "+298 xxx xxx\n" + // Faroe Islands "+299 xxx xxx\n" + // Greenland //////////////////////////////////////////////////////////// // zone 3: europe, southern and small countries //////////////////////////////////////////////////////////// "+30 xxx xxx xxxx\n" + // Greece "+31 6 xxxx xxxx\n" + // Netherlands "+31 xx xxx xxxx\n" + // Netherlands "+31 xxx xx xxxx\n" + // Netherlands "+32 2 xxx xx xx\n" + // Belgium "+32 3 xxx xx xx\n" + // Belgium "+32 4xx xx xx xx\n" + // Belgium "+32 9 xxx xx xx\n" + // Belgium "+32 xx xx xx xx\n" + // Belgium "+33 xxx xxx xxx\n" + // France "+34 xxx xxx xxx\n" + // Spain "+351 3xx xxx xxx\n" + // Portugal "+351 7xx xxx xxx\n" + // Portugal "+351 8xx xxx xxx\n" + // Portugal "+351 xx xxx xxxx\n" + // Portugal "+352 xx xxxx\n" + // Luxembourg "+352 6x1 xxx xxx\n" + // Luxembourg "+352 \n" + // Luxembourg "+353 xxx xxxx\n" + // Ireland "+353 xxxx xxxx\n" + // Ireland "+353 xx xxx xxxx\n" + // Ireland "+354 3xx xxx xxx\n" + // Iceland "+354 xxx xxxx\n" + // Iceland "+355 6x xxx xxxx\n" + // Albania "+355 xxx xxxx\n" + // Albania "+356 xx xx xx xx\n" + // Malta "+357 xx xx xx xx\n" + // Cyprus "+358 \n" + // Finland "+359 \n" + // Bulgaria "+36 1 xxx xxxx\n" + // Hungary "+36 20 xxx xxxx\n" + // Hungary "+36 21 xxx xxxx\n" + // Hungary "+36 30 xxx xxxx\n" + // Hungary "+36 70 xxx xxxx\n" + // Hungary "+36 71 xxx xxxx\n" + // Hungary "+36 xx xxx xxx\n" + // Hungary "+370 6x xxx xxx\n" + // Lithuania "+370 xxx xx xxx\n" + // Lithuania "+371 xxxx xxxx\n" + // Latvia "+372 5 xxx xxxx\n" + // Estonia "+372 xxx xxxx\n" + // Estonia "+373 6xx xx xxx\n" + // Moldova "+373 7xx xx xxx\n" + // Moldova "+373 xxx xxxxx\n" + // Moldova "+374 xx xxx xxx\n" + // Armenia "+375 xx xxx xxxx\n" + // Belarus "+376 xx xx xx\n" + // Andorra "+377 xxxx xxxx\n" + // Monaco "+378 xxx xxx xxxx\n" + // San Marino "+380 xxx xx xx xx\n" + // Ukraine "+381 xx xxx xxxx\n" + // Serbia "+382 xx xxx xxxx\n" + // Montenegro "+385 xx xxx xxxx\n" + // Croatia "+386 x xxx xxxx\n" + // Slovenia "+387 xx xx xx xx\n" + // Bosnia and herzegovina "+389 2 xxx xx xx\n" + // Macedonia "+389 xx xx xx xx\n" + // Macedonia "+39 xxx xxx xxx\n" + // Italy "+39 3xx xxx xxxx\n" + // Italy "+39 xx xxxx xxxx\n" + // Italy //////////////////////////////////////////////////////////// // zone 4: europe, northern countries //////////////////////////////////////////////////////////// "+40 xxx xxx xxx\n" + // Romania "+41 xx xxx xx xx\n" + // Switzerland "+420 xxx xxx xxx\n" + // Czech republic "+421 xxx xxx xxx\n" + // Slovakia "+421 xxx xxx xxxx\n" + // Liechtenstein "+43 \n" + // Austria "+44 xxx xxx xxxx\n" + // UK "+45 xx xx xx xx\n" + // Denmark "+46 \n" + // Sweden "+47 xxxx xxxx\n" + // Norway "+48 xx xxx xxxx\n" + // Poland "+49 1xx xxxx xxx\n" + // Germany "+49 1xx xxxx xxxx\n" + // Germany "+49 \n" + // Germany //////////////////////////////////////////////////////////// // zone 5: latin america //////////////////////////////////////////////////////////// "+50x \n" + "+51 9xx xxx xxx\n" + // Peru "+51 1 xxx xxxx\n" + // Peru "+51 xx xx xxxx\n" + // Peru "+52 1 xxx xxx xxxx\n" + // Mexico "+52 xxx xxx xxxx\n" + // Mexico "+53 xxxx xxxx\n" + // Cuba "+54 9 11 xxxx xxxx\n" + // Argentina "+54 9 xxx xxx xxxx\n" + // Argentina "+54 11 xxxx xxxx\n" + // Argentina "+54 xxx xxx xxxx\n" + // Argentina "+55 xx xxxx xxxx\n" + // Brazil "+56 2 xxxxxx\n" + // Chile "+56 9 xxxx xxxx\n" + // Chile "+56 xx xxxxxx\n" + // Chile "+56 xx xxxxxxx\n" + // Chile "+57 x xxx xxxx\n" + // Columbia "+57 3xx xxx xxxx\n" + // Columbia "+58 xxx xxx xxxx\n" + // Venezuela "+59x \n" + //////////////////////////////////////////////////////////// // zone 6: southeast asia and oceania //////////////////////////////////////////////////////////// // TODO is this right? "+60 3 xxxx xxxx\n" + // Malaysia "+60 8x xxxxxx\n" + // Malaysia "+60 x xxx xxxx\n" + // Malaysia "+60 14 x xxx xxxx\n" + // Malaysia "+60 1x xxx xxxx\n" + // Malaysia "+60 x xxxx xxxx\n" + // Malaysia "+60 \n" + // Malaysia "+61 4xx xxx xxx\n" + // Australia "+61 x xxxx xxxx\n" + // Australia // TODO: is this right? "+62 8xx xxxx xxxx\n" + // Indonesia "+62 21 xxxxx\n" + // Indonesia "+62 xx xxxxxx\n" + // Indonesia "+62 xx xxx xxxx\n" + // Indonesia "+62 xx xxxx xxxx\n" + // Indonesia "+63 2 xxx xxxx\n" + // Phillipines "+63 xx xxx xxxx\n" + // Phillipines "+63 9xx xxx xxxx\n" + // Phillipines // TODO: is this right? "+64 2 xxx xxxx\n" + // New Zealand "+64 2 xxx xxxx x\n" + // New Zealand "+64 2 xxx xxxx xx\n" + // New Zealand "+64 x xxx xxxx\n" + // New Zealand "+65 xxxx xxxx\n" + // Singapore "+66 8 xxxx xxxx\n" + // Thailand "+66 2 xxx xxxx\n" + // Thailand "+66 xx xx xxxx\n" + // Thailand "+67x \n" + "+68x \n" + "+690 x xxx\n" + // Tokelau "+691 xxx xxxx\n" + // Micronesia "+692 xxx xxxx\n" + // marshall Islands //////////////////////////////////////////////////////////// // zone 7: russia and kazakstan //////////////////////////////////////////////////////////// "+7 6xx xx xxxxx\n" + // Kazakstan "+7 7xx 2 xxxxxx\n" + // Kazakstan "+7 7xx xx xxxxx\n" + // Kazakstan "+7 xxx xxx xx xx\n" + // Russia //////////////////////////////////////////////////////////// // zone 8: east asia //////////////////////////////////////////////////////////// "+81 3 xxxx xxxx\n" + // Japan "+81 6 xxxx xxxx\n" + // Japan "+81 xx xxx xxxx\n" + // Japan "+81 x0 xxxx xxxx\n" + // Japan "+82 2 xxx xxxx\n" + // South korea "+82 2 xxxx xxxx\n" + // South korea "+82 xx xxxx xxxx\n" + // South korea "+82 xx xxx xxxx\n" + // South korea "+84 4 xxxx xxxx\n" + // Vietnam "+84 xx xxxx xxx\n" + // Vietnam "+84 xx xxxx xxxx\n" + // Vietnam "+850 \n" + // North Korea "+852 xxxx xxxx\n" + // Hong Kong "+853 xxxx xxxx\n" + // Macau "+855 1x xxx xxx\n" + // Cambodia "+855 9x xxx xxx\n" + // Cambodia "+855 xx xx xx xx\n" + // Cambodia "+856 20 x xxx xxx\n" + // Laos "+856 xx xxx xxx\n" + // Laos "+852 xxxx xxxx\n" + // Hong kong "+86 10 xxxx xxxx\n" + // China "+86 2x xxxx xxxx\n" + // China "+86 xxx xxx xxxx\n" + // China "+86 xxx xxxx xxxx\n" + // China "+880 xx xxxx xxxx\n" + // Bangladesh "+886 \n" + // Taiwan //////////////////////////////////////////////////////////// // zone 9: south asia, west asia, central asia, middle east //////////////////////////////////////////////////////////// "+90 xxx xxx xxxx\n" + // Turkey "+91 9x xx xxxxxx\n" + // India "+91 xx xxxx xxxx\n" + // India "+92 xx xxx xxxx\n" + // Pakistan "+92 3xx xxx xxxx\n" + // Pakistan "+93 70 xxx xxx\n" + // Afghanistan "+93 xx xxx xxxx\n" + // Afghanistan "+94 xx xxx xxxx\n" + // Sri Lanka "+95 1 xxx xxx\n" + // Burma "+95 2 xxx xxx\n" + // Burma "+95 xx xxxxx\n" + // Burma "+95 9 xxx xxxx\n" + // Burma "+960 xxx xxxx\n" + // Maldives "+961 x xxx xxx\n" + // Lebanon "+961 xx xxx xxx\n" + // Lebanon "+962 7 xxxx xxxx\n" + // Jordan "+962 x xxx xxxx\n" + // Jordan "+963 11 xxx xxxx\n" + // Syria "+963 xx xxx xxx\n" + // Syria "+964 \n" + // Iraq "+965 xxxx xxxx\n" + // Kuwait "+966 5x xxx xxxx\n" + // Saudi Arabia "+966 x xxx xxxx\n" + // Saudi Arabia "+967 7xx xxx xxx\n" + // Yemen "+967 x xxx xxx\n" + // Yemen "+968 xxxx xxxx\n" + // Oman "+970 5x xxx xxxx\n" + // Palestinian Authority "+970 x xxx xxxx\n" + // Palestinian Authority "+971 5x xxx xxxx\n" + // United Arab Emirates "+971 x xxx xxxx\n" + // United Arab Emirates "+972 5x xxx xxxx\n" + // Israel "+972 x xxx xxxx\n" + // Israel "+973 xxxx xxxx\n" + // Bahrain "+974 xxx xxxx\n" + // Qatar "+975 1x xxx xxx\n" + // Bhutan "+975 x xxx xxx\n" + // Bhutan "+976 \n" + // Mongolia "+977 xxxx xxxx\n" + // Nepal "+977 98 xxxx xxxx\n" + // Nepal "+98 xxx xxx xxxx\n" + // Iran "+992 xxx xxx xxx\n" + // Tajikistan "+993 xxxx xxxx\n" + // Turkmenistan "+994 xx xxx xxxx\n" + // Azerbaijan "+994 xxx xxxxx\n" + // Azerbaijan "+995 xx xxx xxx\n" + // Georgia "+996 xxx xxx xxx\n" + // Kyrgyzstan "+998 xx xxx xxxx\n"; // Uzbekistan // TODO: need to handle variable number notation private static String formatNumber(String formats, String number) { number = number.trim(); final int nlen = number.length(); final int formatslen = formats.length(); StringBuffer sb = new StringBuffer(); // loop over country codes for (int f = 0; f < formatslen; ) { sb.setLength(0); int n = 0; // loop over letters of pattern while (true) { final char fch = formats.charAt(f); if (fch == '\n' && n >= nlen) return sb.toString(); if (fch == '\n' || n >= nlen) break; final char nch = number.charAt(n); // pattern matches number if (fch == nch || (fch == 'x' && Character.isDigit(nch))) { f++; n++; sb.append(nch); } // don't match ' ' in pattern, but insert into result else if (fch == ' ') { f++; sb.append(' '); // ' ' at end -> match all the rest if (formats.charAt(f) == '\n') { return sb.append(number, n, nlen).toString(); } } // match failed else break; } // step to the next pattern f = formats.indexOf('\n', f) + 1; if (f == 0) break; } return null; } /** * Format a phone number string. * At some point, PhoneNumberUtils.formatNumber will handle this. * @param num phone number string. * @return formatted phone number string. */ private static String formatNumber(String num) { String fmt = null; fmt = formatNumber(mPlusFormats, num); if (fmt != null) return fmt; fmt = formatNumber(mNanpFormats, num); if (fmt != null) return fmt; return null; } /** * Called when recognition succeeds. It receives a list * of results, builds a corresponding list of Intents, and * passes them to the {@link RecognizerClient}, which selects and * performs a corresponding action. * @param recognizerClient the client that will be sent the results */ protected void onRecognitionSuccess(RecognizerClient recognizerClient) throws InterruptedException { if (false) Log.d(TAG, "onRecognitionSuccess"); if (mLogger != null) mLogger.logNbestHeader(); ArrayList<Intent> intents = new ArrayList<Intent>(); int highestConfidence = 0; int examineLimit = RESULT_LIMIT; if (mMinimizeResults) { examineLimit = 1; } for (int result = 0; result < mSrec.getResultCount() && intents.size() < examineLimit; result++) { // parse the semanticMeaning string and build an Intent String conf = mSrec.getResult(result, Recognizer.KEY_CONFIDENCE); String literal = mSrec.getResult(result, Recognizer.KEY_LITERAL); String semantic = mSrec.getResult(result, Recognizer.KEY_MEANING); String msg = "conf=" + conf + " lit=" + literal + " sem=" + semantic; if (false) Log.d(TAG, msg); int confInt = Integer.parseInt(conf); if (highestConfidence < confInt) highestConfidence = confInt; if (confInt < MINIMUM_CONFIDENCE || confInt * 2 < highestConfidence) { if (false) Log.d(TAG, "confidence too low, dropping"); break; } if (mLogger != null) mLogger.logLine(msg); String[] commands = semantic.trim().split(" "); // DIAL 650 867 5309 // DIAL 867 5309 // DIAL 911 if ("DIAL".equalsIgnoreCase(commands[0])) { Uri uri = Uri.fromParts("tel", commands[1], null); String num = formatNumber(commands[1]); if (num != null) { addCallIntent(intents, uri, literal.split(" ")[0].trim() + " " + num, "", 0); } } // CALL JACK JONES else if ("CALL".equalsIgnoreCase(commands[0]) && commands.length >= 7) { // parse the ids long contactId = Long.parseLong(commands[1]); // people table long phoneId = Long.parseLong(commands[2]); // phones table long homeId = Long.parseLong(commands[3]); // phones table long mobileId = Long.parseLong(commands[4]); // phones table long workId = Long.parseLong(commands[5]); // phones table long otherId = Long.parseLong(commands[6]); // phones table Resources res = mActivity.getResources(); int count = 0; // // generate the best entry corresponding to what was said // // 'CALL JACK JONES AT HOME|MOBILE|WORK|OTHER' if (commands.length == 8) { long spokenPhoneId = "H".equalsIgnoreCase(commands[7]) ? homeId : "M".equalsIgnoreCase(commands[7]) ? mobileId : "W".equalsIgnoreCase(commands[7]) ? workId : "O".equalsIgnoreCase(commands[7]) ? otherId : VoiceContact.ID_UNDEFINED; if (spokenPhoneId != VoiceContact.ID_UNDEFINED) { addCallIntent(intents, ContentUris.withAppendedId( Phone.CONTENT_URI, spokenPhoneId), literal, commands[7], 0); count++; } } // 'CALL JACK JONES', with valid default phoneId else if (commands.length == 7) { String phoneType = null; CharSequence phoneIdMsg = null; if (phoneId == VoiceContact.ID_UNDEFINED) { phoneType = null; phoneIdMsg = null; } else if (phoneId == homeId) { phoneType = "H"; phoneIdMsg = res.getText(R.string.at_home); } else if (phoneId == mobileId) { phoneType = "M"; phoneIdMsg = res.getText(R.string.on_mobile); } else if (phoneId == workId) { phoneType = "W"; phoneIdMsg = res.getText(R.string.at_work); } else if (phoneId == otherId) { phoneType = "O"; phoneIdMsg = res.getText(R.string.at_other); } if (phoneIdMsg != null) { addCallIntent(intents, ContentUris.withAppendedId( Phone.CONTENT_URI, phoneId), literal + phoneIdMsg, phoneType, 0); count++; } } if (count == 0 || !mMinimizeResults) { // // generate all other entries for this person // // trim last two words, ie 'at home', etc String lit = literal; if (commands.length == 8) { String[] words = literal.trim().split(" "); StringBuffer sb = new StringBuffer(); for (int i = 0; i < words.length - 2; i++) { if (i != 0) { sb.append(' '); } sb.append(words[i]); } lit = sb.toString(); } // add 'CALL JACK JONES at home' using phoneId if (homeId != VoiceContact.ID_UNDEFINED) { addCallIntent(intents, ContentUris.withAppendedId( Phone.CONTENT_URI, homeId), lit + res.getText(R.string.at_home), "H", 0); count++; } // add 'CALL JACK JONES on mobile' using mobileId if (mobileId != VoiceContact.ID_UNDEFINED) { addCallIntent(intents, ContentUris.withAppendedId( Phone.CONTENT_URI, mobileId), lit + res.getText(R.string.on_mobile), "M", 0); count++; } // add 'CALL JACK JONES at work' using workId if (workId != VoiceContact.ID_UNDEFINED) { addCallIntent(intents, ContentUris.withAppendedId( Phone.CONTENT_URI, workId), lit + res.getText(R.string.at_work), "W", 0); count++; } // add 'CALL JACK JONES at other' using otherId if (otherId != VoiceContact.ID_UNDEFINED) { addCallIntent(intents, ContentUris.withAppendedId( Phone.CONTENT_URI, otherId), lit + res.getText(R.string.at_other), "O", 0); count++; } } // // if no other entries were generated, use the personId // // add 'CALL JACK JONES', with valid personId if (count == 0 && contactId != VoiceContact.ID_UNDEFINED) { // TODO: what should really happen here is, we find // all phones for this contact, and create a label that // says "call person X at phone type Y", and add intents // for each of them to the return list. // It's too late in <del>Gingerbread</del> ICS to add the strings that // would be required for this, so we'll just ignore this person. // See also issue 3090362 and 5551677 // ACTION_CALL_PRIVILEGED does not work with Contacts.CONTENT_URI. // addCallIntent(intents, ContentUris.withAppendedId( // Contacts.CONTENT_URI, contactId), literal, "", 0); } } else if ("X".equalsIgnoreCase(commands[0])) { Intent intent = new Intent(RecognizerEngine.ACTION_RECOGNIZER_RESULT, null); intent.putExtra(RecognizerEngine.SENTENCE_EXTRA, literal); intent.putExtra(RecognizerEngine.SEMANTIC_EXTRA, semantic); addIntent(intents, intent); } // "CALL VoiceMail" else if ("voicemail".equalsIgnoreCase(commands[0]) && commands.length == 1) { addCallIntent(intents, Uri.fromParts("voicemail", "x", null), literal, "", Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); } // "REDIAL" else if ("redial".equalsIgnoreCase(commands[0]) && commands.length == 1) { String number = VoiceContact.redialNumber(mActivity); if (number != null) { addCallIntent(intents, Uri.fromParts("tel", number, null), literal, "", Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); } } // "Intent ..." else if ("Intent".equalsIgnoreCase(commands[0])) { for (int i = 1; i < commands.length; i++) { try { Intent intent = Intent.getIntent(commands[i]); if (intent.getStringExtra(SENTENCE_EXTRA) == null) { intent.putExtra(SENTENCE_EXTRA, literal); } addIntent(intents, intent); } catch (URISyntaxException e) { if (false) { Log.d(TAG, "onRecognitionSuccess: poorly " + "formed URI in grammar" + e); } } } } // "OPEN ..." else if ("OPEN".equalsIgnoreCase(commands[0]) && mAllowOpenEntries) { PackageManager pm = mActivity.getPackageManager(); if (commands.length > 1 & mOpenEntries != null) { // the semantic value is equal to the literal in this case. // We have to do the mapping from this text to the // componentname ourselves. See Bug: 2457238. // The problem is that the list of all componentnames // can be pretty large and overflow the limit that // the recognizer has. String meaning = mOpenEntries.get(commands[1]); String[] components = meaning.trim().split(" "); for (int i=0; i < components.length; i++) { String component = components[i]; Intent intent = new Intent(Intent.ACTION_MAIN); intent.addCategory("android.intent.category.VOICE_LAUNCH"); String packageName = component.substring( 0, component.lastIndexOf('/')); String className = component.substring( component.lastIndexOf('/')+1, component.length()); intent.setClassName(packageName, className); List<ResolveInfo> riList = pm.queryIntentActivities(intent, 0); for (ResolveInfo ri : riList) { String label = ri.loadLabel(pm).toString(); intent = new Intent(Intent.ACTION_MAIN); intent.addCategory("android.intent.category.VOICE_LAUNCH"); intent.setClassName(packageName, className); intent.putExtra(SENTENCE_EXTRA, literal.split(" ")[0] + " " + label); addIntent(intents, intent); } } } } // can't parse result else { if (false) Log.d(TAG, "onRecognitionSuccess: parse error"); } } // log if requested if (mLogger != null) mLogger.logIntents(intents); // bail out if cancelled if (Thread.interrupted()) throw new InterruptedException(); if (intents.size() == 0) { // TODO: strip HOME|MOBILE|WORK and try default here? recognizerClient.onRecognitionFailure("No Intents generated"); } else { recognizerClient.onRecognitionSuccess( intents.toArray(new Intent[intents.size()])); } } // only add if different private static void addCallIntent(ArrayList<Intent> intents, Uri uri, String literal, String phoneType, int flags) { Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, uri). setFlags(flags). putExtra(SENTENCE_EXTRA, literal). putExtra(PHONE_TYPE_EXTRA, phoneType); addIntent(intents, intent); } }