| /* |
| * 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); |
| } |
| } |