From bf4bb0555ca827e660ad05b4caf982a030211c03 Mon Sep 17 00:00:00 2001 From: linyuh Date: Thu, 21 Dec 2017 15:42:00 -0800 Subject: Reorganize classes related to smart dial. Bug: 30215380,70633239 Test: Existing tests PiperOrigin-RevId: 179868033 Change-Id: If8cdbdfafb3a66397623578131649cb8adc18733 --- java/com/android/dialer/app/DialtactsActivity.java | 4 +- .../app/list/SmartDialNumberListAdapter.java | 6 +- .../dialer/app/list/SmartDialSearchFragment.java | 2 +- .../dialer/database/DialerDatabaseHelper.java | 4 +- .../dialer/dialpadview/SmartDialCursorLoader.java | 182 ----- .../cp2/SearchContactsCursorLoader.java | 2 +- .../dialer/smartdial/BulgarianSmartDialMap.java | 91 --- .../dialer/smartdial/CompositeSmartDialMap.java | 172 ----- .../dialer/smartdial/LatinSmartDialMap.java | 785 --------------------- .../dialer/smartdial/RussianSmartDialMap.java | 94 --- .../dialer/smartdial/SmartDialCursorLoader.java | 182 +++++ .../com/android/dialer/smartdial/SmartDialMap.java | 103 --- .../dialer/smartdial/SmartDialMatchPosition.java | 70 -- .../dialer/smartdial/SmartDialNameMatcher.java | 429 ----------- .../android/dialer/smartdial/SmartDialPrefix.java | 601 ---------------- .../dialer/smartdial/UkrainianSmartDialMap.java | 93 --- .../smartdial/map/BulgarianSmartDialMap.java | 91 +++ .../smartdial/map/CompositeSmartDialMap.java | 172 +++++ .../dialer/smartdial/map/LatinSmartDialMap.java | 785 +++++++++++++++++++++ .../dialer/smartdial/map/RussianSmartDialMap.java | 94 +++ .../android/dialer/smartdial/map/SmartDialMap.java | 103 +++ .../smartdial/map/UkrainianSmartDialMap.java | 93 +++ .../smartdial/util/SmartDialMatchPosition.java | 70 ++ .../smartdial/util/SmartDialNameMatcher.java | 430 +++++++++++ .../dialer/smartdial/util/SmartDialPrefix.java | 602 ++++++++++++++++ 25 files changed, 2631 insertions(+), 2629 deletions(-) delete mode 100644 java/com/android/dialer/dialpadview/SmartDialCursorLoader.java delete mode 100644 java/com/android/dialer/smartdial/BulgarianSmartDialMap.java delete mode 100644 java/com/android/dialer/smartdial/CompositeSmartDialMap.java delete mode 100644 java/com/android/dialer/smartdial/LatinSmartDialMap.java delete mode 100644 java/com/android/dialer/smartdial/RussianSmartDialMap.java create mode 100644 java/com/android/dialer/smartdial/SmartDialCursorLoader.java delete mode 100644 java/com/android/dialer/smartdial/SmartDialMap.java delete mode 100644 java/com/android/dialer/smartdial/SmartDialMatchPosition.java delete mode 100644 java/com/android/dialer/smartdial/SmartDialNameMatcher.java delete mode 100644 java/com/android/dialer/smartdial/SmartDialPrefix.java delete mode 100644 java/com/android/dialer/smartdial/UkrainianSmartDialMap.java create mode 100644 java/com/android/dialer/smartdial/map/BulgarianSmartDialMap.java create mode 100644 java/com/android/dialer/smartdial/map/CompositeSmartDialMap.java create mode 100644 java/com/android/dialer/smartdial/map/LatinSmartDialMap.java create mode 100644 java/com/android/dialer/smartdial/map/RussianSmartDialMap.java create mode 100644 java/com/android/dialer/smartdial/map/SmartDialMap.java create mode 100644 java/com/android/dialer/smartdial/map/UkrainianSmartDialMap.java create mode 100644 java/com/android/dialer/smartdial/util/SmartDialMatchPosition.java create mode 100644 java/com/android/dialer/smartdial/util/SmartDialNameMatcher.java create mode 100644 java/com/android/dialer/smartdial/util/SmartDialPrefix.java (limited to 'java/com') diff --git a/java/com/android/dialer/app/DialtactsActivity.java b/java/com/android/dialer/app/DialtactsActivity.java index eb95a4ee4..bbb45943a 100644 --- a/java/com/android/dialer/app/DialtactsActivity.java +++ b/java/com/android/dialer/app/DialtactsActivity.java @@ -133,8 +133,8 @@ import com.android.dialer.searchfragment.list.NewSearchFragment; import com.android.dialer.searchfragment.list.NewSearchFragment.SearchFragmentListener; import com.android.dialer.simulator.Simulator; import com.android.dialer.simulator.SimulatorComponent; -import com.android.dialer.smartdial.SmartDialNameMatcher; -import com.android.dialer.smartdial.SmartDialPrefix; +import com.android.dialer.smartdial.util.SmartDialNameMatcher; +import com.android.dialer.smartdial.util.SmartDialPrefix; import com.android.dialer.storage.StorageComponent; import com.android.dialer.telecom.TelecomUtil; import com.android.dialer.util.DialerUtils; diff --git a/java/com/android/dialer/app/list/SmartDialNumberListAdapter.java b/java/com/android/dialer/app/list/SmartDialNumberListAdapter.java index 5b48ccfd0..1d2cda3ea 100644 --- a/java/com/android/dialer/app/list/SmartDialNumberListAdapter.java +++ b/java/com/android/dialer/app/list/SmartDialNumberListAdapter.java @@ -22,9 +22,9 @@ import android.telephony.PhoneNumberUtils; import android.text.TextUtils; import com.android.contacts.common.list.ContactListItemView; import com.android.dialer.common.LogUtil; -import com.android.dialer.dialpadview.SmartDialCursorLoader; -import com.android.dialer.smartdial.SmartDialMatchPosition; -import com.android.dialer.smartdial.SmartDialNameMatcher; +import com.android.dialer.smartdial.SmartDialCursorLoader; +import com.android.dialer.smartdial.util.SmartDialMatchPosition; +import com.android.dialer.smartdial.util.SmartDialNameMatcher; import com.android.dialer.util.CallUtil; import java.util.ArrayList; diff --git a/java/com/android/dialer/app/list/SmartDialSearchFragment.java b/java/com/android/dialer/app/list/SmartDialSearchFragment.java index e97a16c19..1a7f19515 100644 --- a/java/com/android/dialer/app/list/SmartDialSearchFragment.java +++ b/java/com/android/dialer/app/list/SmartDialSearchFragment.java @@ -31,7 +31,7 @@ import com.android.dialer.app.R; import com.android.dialer.callintent.CallInitiationType; import com.android.dialer.common.LogUtil; import com.android.dialer.database.DialerDatabaseHelper; -import com.android.dialer.dialpadview.SmartDialCursorLoader; +import com.android.dialer.smartdial.SmartDialCursorLoader; import com.android.dialer.util.PermissionsUtil; import com.android.dialer.widget.EmptyContentView; import java.util.Arrays; diff --git a/java/com/android/dialer/database/DialerDatabaseHelper.java b/java/com/android/dialer/database/DialerDatabaseHelper.java index b0bd62a34..3fb87304b 100644 --- a/java/com/android/dialer/database/DialerDatabaseHelper.java +++ b/java/com/android/dialer/database/DialerDatabaseHelper.java @@ -42,8 +42,8 @@ import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.DialerExecutor.Worker; import com.android.dialer.common.concurrent.DialerExecutorComponent; import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns; -import com.android.dialer.smartdial.SmartDialNameMatcher; -import com.android.dialer.smartdial.SmartDialPrefix; +import com.android.dialer.smartdial.util.SmartDialNameMatcher; +import com.android.dialer.smartdial.util.SmartDialPrefix; import com.android.dialer.util.PermissionsUtil; import java.util.ArrayList; import java.util.HashSet; diff --git a/java/com/android/dialer/dialpadview/SmartDialCursorLoader.java b/java/com/android/dialer/dialpadview/SmartDialCursorLoader.java deleted file mode 100644 index d085b55bd..000000000 --- a/java/com/android/dialer/dialpadview/SmartDialCursorLoader.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (C) 2013 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.dialer.dialpadview; - -import android.content.AsyncTaskLoader; -import android.content.Context; -import android.database.Cursor; -import android.database.MatrixCursor; -import com.android.contacts.common.list.PhoneNumberListAdapter.PhoneQuery; -import com.android.dialer.common.LogUtil; -import com.android.dialer.database.Database; -import com.android.dialer.database.DialerDatabaseHelper; -import com.android.dialer.database.DialerDatabaseHelper.ContactNumber; -import com.android.dialer.smartdial.SmartDialNameMatcher; -import com.android.dialer.util.PermissionsUtil; -import java.util.ArrayList; - -/** Implements a Loader class to asynchronously load SmartDial search results. */ -public class SmartDialCursorLoader extends AsyncTaskLoader { - - private static final String TAG = "SmartDialCursorLoader"; - private static final boolean DEBUG = false; - - private final Context mContext; - - private Cursor mCursor; - - private String mQuery; - private SmartDialNameMatcher mNameMatcher; - - private boolean mShowEmptyListForNullQuery = true; - - public SmartDialCursorLoader(Context context) { - super(context); - mContext = context; - } - - /** - * Configures the query string to be used to find SmartDial matches. - * - * @param query The query string user typed. - */ - public void configureQuery(String query) { - if (DEBUG) { - LogUtil.v(TAG, "Configure new query to be " + query); - } - mQuery = SmartDialNameMatcher.normalizeNumber(mContext, query); - - /** Constructs a name matcher object for matching names. */ - mNameMatcher = new SmartDialNameMatcher(mQuery); - mNameMatcher.setShouldMatchEmptyQuery(!mShowEmptyListForNullQuery); - } - - /** - * Queries the SmartDial database and loads results in background. - * - * @return Cursor of contacts that matches the SmartDial query. - */ - @Override - public Cursor loadInBackground() { - if (DEBUG) { - LogUtil.v(TAG, "Load in background " + mQuery); - } - - if (!PermissionsUtil.hasContactsReadPermissions(mContext)) { - return new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY); - } - - /** Loads results from the database helper. */ - final DialerDatabaseHelper dialerDatabaseHelper = - Database.get(mContext).getDatabaseHelper(mContext); - final ArrayList allMatches = - dialerDatabaseHelper.getLooseMatches(mQuery, mNameMatcher); - - if (DEBUG) { - LogUtil.v(TAG, "Loaded matches " + allMatches.size()); - } - - /** Constructs a cursor for the returned array of results. */ - final MatrixCursor cursor = new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY); - Object[] row = new Object[PhoneQuery.PROJECTION_PRIMARY.length]; - for (ContactNumber contact : allMatches) { - row[PhoneQuery.PHONE_ID] = contact.dataId; - row[PhoneQuery.PHONE_NUMBER] = contact.phoneNumber; - row[PhoneQuery.CONTACT_ID] = contact.id; - row[PhoneQuery.LOOKUP_KEY] = contact.lookupKey; - row[PhoneQuery.PHOTO_ID] = contact.photoId; - row[PhoneQuery.DISPLAY_NAME] = contact.displayName; - row[PhoneQuery.CARRIER_PRESENCE] = contact.carrierPresence; - cursor.addRow(row); - } - return cursor; - } - - @Override - public void deliverResult(Cursor cursor) { - if (isReset()) { - /** The Loader has been reset; ignore the result and invalidate the data. */ - releaseResources(cursor); - return; - } - - /** Hold a reference to the old data so it doesn't get garbage collected. */ - Cursor oldCursor = mCursor; - mCursor = cursor; - - if (isStarted()) { - /** If the Loader is in a started state, deliver the results to the client. */ - super.deliverResult(cursor); - } - - /** Invalidate the old data as we don't need it any more. */ - if (oldCursor != null && oldCursor != cursor) { - releaseResources(oldCursor); - } - } - - @Override - protected void onStartLoading() { - if (mCursor != null) { - /** Deliver any previously loaded data immediately. */ - deliverResult(mCursor); - } - if (mCursor == null) { - /** Force loads every time as our results change with queries. */ - forceLoad(); - } - } - - @Override - protected void onStopLoading() { - /** The Loader is in a stopped state, so we should attempt to cancel the current load. */ - cancelLoad(); - } - - @Override - protected void onReset() { - /** Ensure the loader has been stopped. */ - onStopLoading(); - - /** Release all previously saved query results. */ - if (mCursor != null) { - releaseResources(mCursor); - mCursor = null; - } - } - - @Override - public void onCanceled(Cursor cursor) { - super.onCanceled(cursor); - - /** The load has been canceled, so we should release the resources associated with 'data'. */ - releaseResources(cursor); - } - - private void releaseResources(Cursor cursor) { - if (cursor != null) { - cursor.close(); - } - } - - public void setShowEmptyListForNullQuery(boolean show) { - mShowEmptyListForNullQuery = show; - if (mNameMatcher != null) { - mNameMatcher.setShouldMatchEmptyQuery(!show); - } - } -} diff --git a/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java b/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java index 23e3f9d88..23f368f54 100644 --- a/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java +++ b/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java @@ -28,9 +28,9 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import com.android.contacts.common.preference.ContactsPreferences; -import com.android.dialer.dialpadview.SmartDialCursorLoader; import com.android.dialer.searchfragment.common.Projections; import com.android.dialer.searchfragment.common.SearchCursor; +import com.android.dialer.smartdial.SmartDialCursorLoader; /** Cursor Loader for CP2 contacts. */ public final class SearchContactsCursorLoader extends CursorLoader { diff --git a/java/com/android/dialer/smartdial/BulgarianSmartDialMap.java b/java/com/android/dialer/smartdial/BulgarianSmartDialMap.java deleted file mode 100644 index a16d15919..000000000 --- a/java/com/android/dialer/smartdial/BulgarianSmartDialMap.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (C) 2017 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.dialer.smartdial; - -import android.support.v4.util.SimpleArrayMap; -import com.google.common.base.Optional; - -/** A {@link SmartDialMap} for the Bulgarian alphabet. */ -@SuppressWarnings("Guava") -final class BulgarianSmartDialMap extends SmartDialMap { - private static final SimpleArrayMap CHAR_TO_KEY_MAP = - new SimpleArrayMap<>(); - - // Reference: https://en.wikipedia.org/wiki/Bulgarian_alphabet - static { - CHAR_TO_KEY_MAP.put('а', '2'); - CHAR_TO_KEY_MAP.put('б', '2'); - CHAR_TO_KEY_MAP.put('в', '2'); - CHAR_TO_KEY_MAP.put('г', '2'); - - CHAR_TO_KEY_MAP.put('д', '3'); - CHAR_TO_KEY_MAP.put('е', '3'); - CHAR_TO_KEY_MAP.put('ж', '3'); - CHAR_TO_KEY_MAP.put('з', '3'); - - CHAR_TO_KEY_MAP.put('и', '4'); - CHAR_TO_KEY_MAP.put('й', '4'); - CHAR_TO_KEY_MAP.put('к', '4'); - CHAR_TO_KEY_MAP.put('л', '4'); - - CHAR_TO_KEY_MAP.put('м', '5'); - CHAR_TO_KEY_MAP.put('н', '5'); - CHAR_TO_KEY_MAP.put('о', '5'); - - CHAR_TO_KEY_MAP.put('п', '6'); - CHAR_TO_KEY_MAP.put('р', '6'); - CHAR_TO_KEY_MAP.put('с', '6'); - - CHAR_TO_KEY_MAP.put('т', '7'); - CHAR_TO_KEY_MAP.put('у', '7'); - CHAR_TO_KEY_MAP.put('ф', '7'); - CHAR_TO_KEY_MAP.put('х', '7'); - - CHAR_TO_KEY_MAP.put('ц', '8'); - CHAR_TO_KEY_MAP.put('ч', '8'); - CHAR_TO_KEY_MAP.put('ш', '8'); - CHAR_TO_KEY_MAP.put('щ', '8'); - - CHAR_TO_KEY_MAP.put('ъ', '9'); - CHAR_TO_KEY_MAP.put('ь', '9'); - CHAR_TO_KEY_MAP.put('ю', '9'); - CHAR_TO_KEY_MAP.put('я', '9'); - } - - private static BulgarianSmartDialMap instance; - - static BulgarianSmartDialMap getInstance() { - if (instance == null) { - instance = new BulgarianSmartDialMap(); - } - - return instance; - } - - private BulgarianSmartDialMap() {} - - @Override - Optional normalizeCharacter(char ch) { - ch = Character.toLowerCase(ch); - return isValidDialpadAlphabeticChar(ch) ? Optional.of(ch) : Optional.absent(); - } - - @Override - SimpleArrayMap getCharToKeyMap() { - return CHAR_TO_KEY_MAP; - } -} diff --git a/java/com/android/dialer/smartdial/CompositeSmartDialMap.java b/java/com/android/dialer/smartdial/CompositeSmartDialMap.java deleted file mode 100644 index e9f237f01..000000000 --- a/java/com/android/dialer/smartdial/CompositeSmartDialMap.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright (C) 2017 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.dialer.smartdial; - -import android.content.Context; -import android.support.annotation.VisibleForTesting; -import android.support.v4.util.SimpleArrayMap; -import com.android.dialer.compat.CompatUtils; -import com.android.dialer.configprovider.ConfigProviderBindings; -import com.google.common.base.Optional; - -/** - * A utility class that combines the functionality of two implementations of {@link SmartDialMap} so - * that we support smart dial for dual alphabets. - * - *

Of the two implementations of {@link SmartDialMap}, the default one always takes precedence. - * The second one is consulted only when the default one is unable to provide a valid result. - * - *

Note that the second implementation can be absent if it is not defined for the system's 1st - * language preference. - */ -@SuppressWarnings("Guava") -public class CompositeSmartDialMap { - @VisibleForTesting - public static final String FLAG_ENABLE_DUAL_ALPHABETS = "enable_dual_alphabets_on_t9"; - - private static final SmartDialMap DEFAULT_MAP = LatinSmartDialMap.getInstance(); - - // A map in which each key is an ISO 639-2 language code and the corresponding value is a - // SmartDialMap - private static final SimpleArrayMap EXTRA_MAPS = new SimpleArrayMap<>(); - - static { - EXTRA_MAPS.put("bul", BulgarianSmartDialMap.getInstance()); - EXTRA_MAPS.put("rus", RussianSmartDialMap.getInstance()); - EXTRA_MAPS.put("ukr", UkrainianSmartDialMap.getInstance()); - } - - private CompositeSmartDialMap() {} - - /** - * Returns true if the provided character can be mapped to a key on the dialpad. - * - *

The provided character is expected to be a normalized character. See {@link - * SmartDialMap#normalizeCharacter(char)} for details. - */ - static boolean isValidDialpadCharacter(Context context, char ch) { - if (DEFAULT_MAP.isValidDialpadCharacter(ch)) { - return true; - } - - Optional extraMap = getExtraMap(context); - return extraMap.isPresent() && extraMap.get().isValidDialpadCharacter(ch); - } - - /** - * Returns true if the provided character is a letter, and can be mapped to a key on the dialpad. - * - *

The provided character is expected to be a normalized character. See {@link - * SmartDialMap#normalizeCharacter(char)} for details. - */ - static boolean isValidDialpadAlphabeticChar(Context context, char ch) { - if (DEFAULT_MAP.isValidDialpadAlphabeticChar(ch)) { - return true; - } - - Optional extraMap = getExtraMap(context); - return extraMap.isPresent() && extraMap.get().isValidDialpadAlphabeticChar(ch); - } - - /** - * Returns true if the provided character is a digit, and can be mapped to a key on the dialpad. - */ - static boolean isValidDialpadNumericChar(Context context, char ch) { - if (DEFAULT_MAP.isValidDialpadNumericChar(ch)) { - return true; - } - - Optional extraMap = getExtraMap(context); - return extraMap.isPresent() && extraMap.get().isValidDialpadNumericChar(ch); - } - - /** - * Get the index of the key on the dialpad which the character corresponds to. - * - *

The provided character is expected to be a normalized character. See {@link - * SmartDialMap#normalizeCharacter(char)} for details. - * - *

If the provided character can't be mapped to a key on the dialpad, return -1. - */ - static byte getDialpadIndex(Context context, char ch) { - Optional dialpadIndex = DEFAULT_MAP.getDialpadIndex(ch); - if (dialpadIndex.isPresent()) { - return dialpadIndex.get(); - } - - Optional extraMap = getExtraMap(context); - if (extraMap.isPresent()) { - dialpadIndex = extraMap.get().getDialpadIndex(ch); - } - - return dialpadIndex.isPresent() ? dialpadIndex.get() : -1; - } - - /** - * Get the actual numeric character on the dialpad which the character corresponds to. - * - *

The provided character is expected to be a normalized character. See {@link - * SmartDialMap#normalizeCharacter(char)} for details. - * - *

If the provided character can't be mapped to a key on the dialpad, return the character. - */ - static char getDialpadNumericCharacter(Context context, char ch) { - Optional dialpadNumericChar = DEFAULT_MAP.getDialpadNumericCharacter(ch); - if (dialpadNumericChar.isPresent()) { - return dialpadNumericChar.get(); - } - - Optional extraMap = getExtraMap(context); - if (extraMap.isPresent()) { - dialpadNumericChar = extraMap.get().getDialpadNumericCharacter(ch); - } - - return dialpadNumericChar.isPresent() ? dialpadNumericChar.get() : ch; - } - - /** - * Converts uppercase characters to lower case ones, and on a best effort basis, strips accents - * from accented characters. - * - *

If the provided character can't be mapped to a key on the dialpad, return the character. - */ - static char normalizeCharacter(Context context, char ch) { - Optional normalizedChar = DEFAULT_MAP.normalizeCharacter(ch); - if (normalizedChar.isPresent()) { - return normalizedChar.get(); - } - - Optional extraMap = getExtraMap(context); - if (extraMap.isPresent()) { - normalizedChar = extraMap.get().normalizeCharacter(ch); - } - - return normalizedChar.isPresent() ? normalizedChar.get() : ch; - } - - @VisibleForTesting - static Optional getExtraMap(Context context) { - if (!ConfigProviderBindings.get(context).getBoolean(FLAG_ENABLE_DUAL_ALPHABETS, false)) { - return Optional.absent(); - } - - String languageCode = CompatUtils.getLocale(context).getISO3Language(); - return EXTRA_MAPS.containsKey(languageCode) - ? Optional.of(EXTRA_MAPS.get(languageCode)) - : Optional.absent(); - } -} diff --git a/java/com/android/dialer/smartdial/LatinSmartDialMap.java b/java/com/android/dialer/smartdial/LatinSmartDialMap.java deleted file mode 100644 index b67901bbe..000000000 --- a/java/com/android/dialer/smartdial/LatinSmartDialMap.java +++ /dev/null @@ -1,785 +0,0 @@ -/* - * Copyright (C) 2016 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.dialer.smartdial; - -import android.support.v4.util.SimpleArrayMap; -import com.google.common.base.Optional; - -/** A {@link SmartDialMap} for the Latin alphabet, which is for T9 dialpad searching. */ -@SuppressWarnings("Guava") -final class LatinSmartDialMap extends SmartDialMap { - private static final SimpleArrayMap CHAR_TO_KEY_MAP = - new SimpleArrayMap<>(); - - static { - CHAR_TO_KEY_MAP.put('a', '2'); - CHAR_TO_KEY_MAP.put('b', '2'); - CHAR_TO_KEY_MAP.put('c', '2'); - - CHAR_TO_KEY_MAP.put('d', '3'); - CHAR_TO_KEY_MAP.put('e', '3'); - CHAR_TO_KEY_MAP.put('f', '3'); - - CHAR_TO_KEY_MAP.put('g', '4'); - CHAR_TO_KEY_MAP.put('h', '4'); - CHAR_TO_KEY_MAP.put('i', '4'); - - CHAR_TO_KEY_MAP.put('j', '5'); - CHAR_TO_KEY_MAP.put('k', '5'); - CHAR_TO_KEY_MAP.put('l', '5'); - - CHAR_TO_KEY_MAP.put('m', '6'); - CHAR_TO_KEY_MAP.put('n', '6'); - CHAR_TO_KEY_MAP.put('o', '6'); - - CHAR_TO_KEY_MAP.put('p', '7'); - CHAR_TO_KEY_MAP.put('q', '7'); - CHAR_TO_KEY_MAP.put('r', '7'); - CHAR_TO_KEY_MAP.put('s', '7'); - - CHAR_TO_KEY_MAP.put('t', '8'); - CHAR_TO_KEY_MAP.put('u', '8'); - CHAR_TO_KEY_MAP.put('v', '8'); - - CHAR_TO_KEY_MAP.put('w', '9'); - CHAR_TO_KEY_MAP.put('x', '9'); - CHAR_TO_KEY_MAP.put('y', '9'); - CHAR_TO_KEY_MAP.put('z', '9'); - } - - private static LatinSmartDialMap instance; - - static LatinSmartDialMap getInstance() { - if (instance == null) { - instance = new LatinSmartDialMap(); - } - - return instance; - } - - private LatinSmartDialMap() {} - - /* - * The switch statement in this function was generated using the python code: - * from unidecode import unidecode - * for i in range(192, 564): - * char = unichr(i) - * decoded = unidecode(char) - * # Unicode characters that decompose into multiple characters i.e. - * # into ss are not supported for now - * if (len(decoded) == 1 and decoded.isalpha()): - * print "case '" + char + "': return Optional.of('" + unidecode(char) + "');" - * - * This gives us a way to map characters containing accents/diacritics to their - * alphabetic equivalents. The unidecode library can be found at: - * http://pypi.python.org/pypi/Unidecode/0.04.1 - * - * Also remaps all upper case latin characters to their lower case equivalents. - */ - @Override - Optional normalizeCharacter(char ch) { - if (isValidDialpadAlphabeticChar(ch)) { - return Optional.of(ch); - } - - switch (ch) { - case 'À': - return Optional.of('a'); - case 'Á': - return Optional.of('a'); - case 'Â': - return Optional.of('a'); - case 'Ã': - return Optional.of('a'); - case 'Ä': - return Optional.of('a'); - case 'Å': - return Optional.of('a'); - case 'Ç': - return Optional.of('c'); - case 'È': - return Optional.of('e'); - case 'É': - return Optional.of('e'); - case 'Ê': - return Optional.of('e'); - case 'Ë': - return Optional.of('e'); - case 'Ì': - return Optional.of('i'); - case 'Í': - return Optional.of('i'); - case 'Î': - return Optional.of('i'); - case 'Ï': - return Optional.of('i'); - case 'Ð': - return Optional.of('d'); - case 'Ñ': - return Optional.of('n'); - case 'Ò': - return Optional.of('o'); - case 'Ó': - return Optional.of('o'); - case 'Ô': - return Optional.of('o'); - case 'Õ': - return Optional.of('o'); - case 'Ö': - return Optional.of('o'); - case '×': - return Optional.of('x'); - case 'Ø': - return Optional.of('o'); - case 'Ù': - return Optional.of('u'); - case 'Ú': - return Optional.of('u'); - case 'Û': - return Optional.of('u'); - case 'Ü': - return Optional.of('u'); - case 'Ý': - return Optional.of('u'); - case 'à': - return Optional.of('a'); - case 'á': - return Optional.of('a'); - case 'â': - return Optional.of('a'); - case 'ã': - return Optional.of('a'); - case 'ä': - return Optional.of('a'); - case 'å': - return Optional.of('a'); - case 'ç': - return Optional.of('c'); - case 'è': - return Optional.of('e'); - case 'é': - return Optional.of('e'); - case 'ê': - return Optional.of('e'); - case 'ë': - return Optional.of('e'); - case 'ì': - return Optional.of('i'); - case 'í': - return Optional.of('i'); - case 'î': - return Optional.of('i'); - case 'ï': - return Optional.of('i'); - case 'ð': - return Optional.of('d'); - case 'ñ': - return Optional.of('n'); - case 'ò': - return Optional.of('o'); - case 'ó': - return Optional.of('o'); - case 'ô': - return Optional.of('o'); - case 'õ': - return Optional.of('o'); - case 'ö': - return Optional.of('o'); - case 'ø': - return Optional.of('o'); - case 'ù': - return Optional.of('u'); - case 'ú': - return Optional.of('u'); - case 'û': - return Optional.of('u'); - case 'ü': - return Optional.of('u'); - case 'ý': - return Optional.of('y'); - case 'ÿ': - return Optional.of('y'); - case 'Ā': - return Optional.of('a'); - case 'ā': - return Optional.of('a'); - case 'Ă': - return Optional.of('a'); - case 'ă': - return Optional.of('a'); - case 'Ą': - return Optional.of('a'); - case 'ą': - return Optional.of('a'); - case 'Ć': - return Optional.of('c'); - case 'ć': - return Optional.of('c'); - case 'Ĉ': - return Optional.of('c'); - case 'ĉ': - return Optional.of('c'); - case 'Ċ': - return Optional.of('c'); - case 'ċ': - return Optional.of('c'); - case 'Č': - return Optional.of('c'); - case 'č': - return Optional.of('c'); - case 'Ď': - return Optional.of('d'); - case 'ď': - return Optional.of('d'); - case 'Đ': - return Optional.of('d'); - case 'đ': - return Optional.of('d'); - case 'Ē': - return Optional.of('e'); - case 'ē': - return Optional.of('e'); - case 'Ĕ': - return Optional.of('e'); - case 'ĕ': - return Optional.of('e'); - case 'Ė': - return Optional.of('e'); - case 'ė': - return Optional.of('e'); - case 'Ę': - return Optional.of('e'); - case 'ę': - return Optional.of('e'); - case 'Ě': - return Optional.of('e'); - case 'ě': - return Optional.of('e'); - case 'Ĝ': - return Optional.of('g'); - case 'ĝ': - return Optional.of('g'); - case 'Ğ': - return Optional.of('g'); - case 'ğ': - return Optional.of('g'); - case 'Ġ': - return Optional.of('g'); - case 'ġ': - return Optional.of('g'); - case 'Ģ': - return Optional.of('g'); - case 'ģ': - return Optional.of('g'); - case 'Ĥ': - return Optional.of('h'); - case 'ĥ': - return Optional.of('h'); - case 'Ħ': - return Optional.of('h'); - case 'ħ': - return Optional.of('h'); - case 'Ĩ': - return Optional.of('i'); - case 'ĩ': - return Optional.of('i'); - case 'Ī': - return Optional.of('i'); - case 'ī': - return Optional.of('i'); - case 'Ĭ': - return Optional.of('i'); - case 'ĭ': - return Optional.of('i'); - case 'Į': - return Optional.of('i'); - case 'į': - return Optional.of('i'); - case 'İ': - return Optional.of('i'); - case 'ı': - return Optional.of('i'); - case 'Ĵ': - return Optional.of('j'); - case 'ĵ': - return Optional.of('j'); - case 'Ķ': - return Optional.of('k'); - case 'ķ': - return Optional.of('k'); - case 'ĸ': - return Optional.of('k'); - case 'Ĺ': - return Optional.of('l'); - case 'ĺ': - return Optional.of('l'); - case 'Ļ': - return Optional.of('l'); - case 'ļ': - return Optional.of('l'); - case 'Ľ': - return Optional.of('l'); - case 'ľ': - return Optional.of('l'); - case 'Ŀ': - return Optional.of('l'); - case 'ŀ': - return Optional.of('l'); - case 'Ł': - return Optional.of('l'); - case 'ł': - return Optional.of('l'); - case 'Ń': - return Optional.of('n'); - case 'ń': - return Optional.of('n'); - case 'Ņ': - return Optional.of('n'); - case 'ņ': - return Optional.of('n'); - case 'Ň': - return Optional.of('n'); - case 'ň': - return Optional.of('n'); - case 'Ō': - return Optional.of('o'); - case 'ō': - return Optional.of('o'); - case 'Ŏ': - return Optional.of('o'); - case 'ŏ': - return Optional.of('o'); - case 'Ő': - return Optional.of('o'); - case 'ő': - return Optional.of('o'); - case 'Ŕ': - return Optional.of('r'); - case 'ŕ': - return Optional.of('r'); - case 'Ŗ': - return Optional.of('r'); - case 'ŗ': - return Optional.of('r'); - case 'Ř': - return Optional.of('r'); - case 'ř': - return Optional.of('r'); - case 'Ś': - return Optional.of('s'); - case 'ś': - return Optional.of('s'); - case 'Ŝ': - return Optional.of('s'); - case 'ŝ': - return Optional.of('s'); - case 'Ş': - return Optional.of('s'); - case 'ş': - return Optional.of('s'); - case 'Š': - return Optional.of('s'); - case 'š': - return Optional.of('s'); - case 'Ţ': - return Optional.of('t'); - case 'ţ': - return Optional.of('t'); - case 'Ť': - return Optional.of('t'); - case 'ť': - return Optional.of('t'); - case 'Ŧ': - return Optional.of('t'); - case 'ŧ': - return Optional.of('t'); - case 'Ũ': - return Optional.of('u'); - case 'ũ': - return Optional.of('u'); - case 'Ū': - return Optional.of('u'); - case 'ū': - return Optional.of('u'); - case 'Ŭ': - return Optional.of('u'); - case 'ŭ': - return Optional.of('u'); - case 'Ů': - return Optional.of('u'); - case 'ů': - return Optional.of('u'); - case 'Ű': - return Optional.of('u'); - case 'ű': - return Optional.of('u'); - case 'Ų': - return Optional.of('u'); - case 'ų': - return Optional.of('u'); - case 'Ŵ': - return Optional.of('w'); - case 'ŵ': - return Optional.of('w'); - case 'Ŷ': - return Optional.of('y'); - case 'ŷ': - return Optional.of('y'); - case 'Ÿ': - return Optional.of('y'); - case 'Ź': - return Optional.of('z'); - case 'ź': - return Optional.of('z'); - case 'Ż': - return Optional.of('z'); - case 'ż': - return Optional.of('z'); - case 'Ž': - return Optional.of('z'); - case 'ž': - return Optional.of('z'); - case 'ſ': - return Optional.of('s'); - case 'ƀ': - return Optional.of('b'); - case 'Ɓ': - return Optional.of('b'); - case 'Ƃ': - return Optional.of('b'); - case 'ƃ': - return Optional.of('b'); - case 'Ɔ': - return Optional.of('o'); - case 'Ƈ': - return Optional.of('c'); - case 'ƈ': - return Optional.of('c'); - case 'Ɖ': - return Optional.of('d'); - case 'Ɗ': - return Optional.of('d'); - case 'Ƌ': - return Optional.of('d'); - case 'ƌ': - return Optional.of('d'); - case 'ƍ': - return Optional.of('d'); - case 'Ɛ': - return Optional.of('e'); - case 'Ƒ': - return Optional.of('f'); - case 'ƒ': - return Optional.of('f'); - case 'Ɠ': - return Optional.of('g'); - case 'Ɣ': - return Optional.of('g'); - case 'Ɩ': - return Optional.of('i'); - case 'Ɨ': - return Optional.of('i'); - case 'Ƙ': - return Optional.of('k'); - case 'ƙ': - return Optional.of('k'); - case 'ƚ': - return Optional.of('l'); - case 'ƛ': - return Optional.of('l'); - case 'Ɯ': - return Optional.of('w'); - case 'Ɲ': - return Optional.of('n'); - case 'ƞ': - return Optional.of('n'); - case 'Ɵ': - return Optional.of('o'); - case 'Ơ': - return Optional.of('o'); - case 'ơ': - return Optional.of('o'); - case 'Ƥ': - return Optional.of('p'); - case 'ƥ': - return Optional.of('p'); - case 'ƫ': - return Optional.of('t'); - case 'Ƭ': - return Optional.of('t'); - case 'ƭ': - return Optional.of('t'); - case 'Ʈ': - return Optional.of('t'); - case 'Ư': - return Optional.of('u'); - case 'ư': - return Optional.of('u'); - case 'Ʊ': - return Optional.of('y'); - case 'Ʋ': - return Optional.of('v'); - case 'Ƴ': - return Optional.of('y'); - case 'ƴ': - return Optional.of('y'); - case 'Ƶ': - return Optional.of('z'); - case 'ƶ': - return Optional.of('z'); - case 'ƿ': - return Optional.of('w'); - case 'Ǎ': - return Optional.of('a'); - case 'ǎ': - return Optional.of('a'); - case 'Ǐ': - return Optional.of('i'); - case 'ǐ': - return Optional.of('i'); - case 'Ǒ': - return Optional.of('o'); - case 'ǒ': - return Optional.of('o'); - case 'Ǔ': - return Optional.of('u'); - case 'ǔ': - return Optional.of('u'); - case 'Ǖ': - return Optional.of('u'); - case 'ǖ': - return Optional.of('u'); - case 'Ǘ': - return Optional.of('u'); - case 'ǘ': - return Optional.of('u'); - case 'Ǚ': - return Optional.of('u'); - case 'ǚ': - return Optional.of('u'); - case 'Ǜ': - return Optional.of('u'); - case 'ǜ': - return Optional.of('u'); - case 'Ǟ': - return Optional.of('a'); - case 'ǟ': - return Optional.of('a'); - case 'Ǡ': - return Optional.of('a'); - case 'ǡ': - return Optional.of('a'); - case 'Ǥ': - return Optional.of('g'); - case 'ǥ': - return Optional.of('g'); - case 'Ǧ': - return Optional.of('g'); - case 'ǧ': - return Optional.of('g'); - case 'Ǩ': - return Optional.of('k'); - case 'ǩ': - return Optional.of('k'); - case 'Ǫ': - return Optional.of('o'); - case 'ǫ': - return Optional.of('o'); - case 'Ǭ': - return Optional.of('o'); - case 'ǭ': - return Optional.of('o'); - case 'ǰ': - return Optional.of('j'); - case 'Dz': - return Optional.of('d'); - case 'Ǵ': - return Optional.of('g'); - case 'ǵ': - return Optional.of('g'); - case 'Ƿ': - return Optional.of('w'); - case 'Ǹ': - return Optional.of('n'); - case 'ǹ': - return Optional.of('n'); - case 'Ǻ': - return Optional.of('a'); - case 'ǻ': - return Optional.of('a'); - case 'Ǿ': - return Optional.of('o'); - case 'ǿ': - return Optional.of('o'); - case 'Ȁ': - return Optional.of('a'); - case 'ȁ': - return Optional.of('a'); - case 'Ȃ': - return Optional.of('a'); - case 'ȃ': - return Optional.of('a'); - case 'Ȅ': - return Optional.of('e'); - case 'ȅ': - return Optional.of('e'); - case 'Ȇ': - return Optional.of('e'); - case 'ȇ': - return Optional.of('e'); - case 'Ȉ': - return Optional.of('i'); - case 'ȉ': - return Optional.of('i'); - case 'Ȋ': - return Optional.of('i'); - case 'ȋ': - return Optional.of('i'); - case 'Ȍ': - return Optional.of('o'); - case 'ȍ': - return Optional.of('o'); - case 'Ȏ': - return Optional.of('o'); - case 'ȏ': - return Optional.of('o'); - case 'Ȑ': - return Optional.of('r'); - case 'ȑ': - return Optional.of('r'); - case 'Ȓ': - return Optional.of('r'); - case 'ȓ': - return Optional.of('r'); - case 'Ȕ': - return Optional.of('u'); - case 'ȕ': - return Optional.of('u'); - case 'Ȗ': - return Optional.of('u'); - case 'ȗ': - return Optional.of('u'); - case 'Ș': - return Optional.of('s'); - case 'ș': - return Optional.of('s'); - case 'Ț': - return Optional.of('t'); - case 'ț': - return Optional.of('t'); - case 'Ȝ': - return Optional.of('y'); - case 'ȝ': - return Optional.of('y'); - case 'Ȟ': - return Optional.of('h'); - case 'ȟ': - return Optional.of('h'); - case 'Ȥ': - return Optional.of('z'); - case 'ȥ': - return Optional.of('z'); - case 'Ȧ': - return Optional.of('a'); - case 'ȧ': - return Optional.of('a'); - case 'Ȩ': - return Optional.of('e'); - case 'ȩ': - return Optional.of('e'); - case 'Ȫ': - return Optional.of('o'); - case 'ȫ': - return Optional.of('o'); - case 'Ȭ': - return Optional.of('o'); - case 'ȭ': - return Optional.of('o'); - case 'Ȯ': - return Optional.of('o'); - case 'ȯ': - return Optional.of('o'); - case 'Ȱ': - return Optional.of('o'); - case 'ȱ': - return Optional.of('o'); - case 'Ȳ': - return Optional.of('y'); - case 'ȳ': - return Optional.of('y'); - case 'A': - return Optional.of('a'); - case 'B': - return Optional.of('b'); - case 'C': - return Optional.of('c'); - case 'D': - return Optional.of('d'); - case 'E': - return Optional.of('e'); - case 'F': - return Optional.of('f'); - case 'G': - return Optional.of('g'); - case 'H': - return Optional.of('h'); - case 'I': - return Optional.of('i'); - case 'J': - return Optional.of('j'); - case 'K': - return Optional.of('k'); - case 'L': - return Optional.of('l'); - case 'M': - return Optional.of('m'); - case 'N': - return Optional.of('n'); - case 'O': - return Optional.of('o'); - case 'P': - return Optional.of('p'); - case 'Q': - return Optional.of('q'); - case 'R': - return Optional.of('r'); - case 'S': - return Optional.of('s'); - case 'T': - return Optional.of('t'); - case 'U': - return Optional.of('u'); - case 'V': - return Optional.of('v'); - case 'W': - return Optional.of('w'); - case 'X': - return Optional.of('x'); - case 'Y': - return Optional.of('y'); - case 'Z': - return Optional.of('z'); - default: - return Optional.absent(); - } - } - - @Override - SimpleArrayMap getCharToKeyMap() { - return CHAR_TO_KEY_MAP; - } -} diff --git a/java/com/android/dialer/smartdial/RussianSmartDialMap.java b/java/com/android/dialer/smartdial/RussianSmartDialMap.java deleted file mode 100644 index ada9182e1..000000000 --- a/java/com/android/dialer/smartdial/RussianSmartDialMap.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) 2017 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.dialer.smartdial; - -import android.support.v4.util.SimpleArrayMap; -import com.google.common.base.Optional; - -/** A {@link SmartDialMap} for the Russian alphabet. */ -@SuppressWarnings("Guava") -final class RussianSmartDialMap extends SmartDialMap { - private static final SimpleArrayMap CHAR_TO_KEY_MAP = - new SimpleArrayMap<>(); - - // Reference: https://en.wikipedia.org/wiki/Russian_alphabet - static { - CHAR_TO_KEY_MAP.put('а', '2'); - CHAR_TO_KEY_MAP.put('б', '2'); - CHAR_TO_KEY_MAP.put('в', '2'); - CHAR_TO_KEY_MAP.put('г', '2'); - - CHAR_TO_KEY_MAP.put('д', '3'); - CHAR_TO_KEY_MAP.put('е', '3'); - CHAR_TO_KEY_MAP.put('ё', '3'); - CHAR_TO_KEY_MAP.put('ж', '3'); - CHAR_TO_KEY_MAP.put('з', '3'); - - CHAR_TO_KEY_MAP.put('и', '4'); - CHAR_TO_KEY_MAP.put('й', '4'); - CHAR_TO_KEY_MAP.put('к', '4'); - CHAR_TO_KEY_MAP.put('л', '4'); - - CHAR_TO_KEY_MAP.put('м', '5'); - CHAR_TO_KEY_MAP.put('н', '5'); - CHAR_TO_KEY_MAP.put('о', '5'); - CHAR_TO_KEY_MAP.put('п', '5'); - - CHAR_TO_KEY_MAP.put('р', '6'); - CHAR_TO_KEY_MAP.put('с', '6'); - CHAR_TO_KEY_MAP.put('т', '6'); - CHAR_TO_KEY_MAP.put('у', '6'); - - CHAR_TO_KEY_MAP.put('ф', '7'); - CHAR_TO_KEY_MAP.put('х', '7'); - CHAR_TO_KEY_MAP.put('ц', '7'); - CHAR_TO_KEY_MAP.put('ч', '7'); - - CHAR_TO_KEY_MAP.put('ш', '8'); - CHAR_TO_KEY_MAP.put('щ', '8'); - CHAR_TO_KEY_MAP.put('ъ', '8'); - CHAR_TO_KEY_MAP.put('ы', '8'); - - CHAR_TO_KEY_MAP.put('ь', '9'); - CHAR_TO_KEY_MAP.put('э', '9'); - CHAR_TO_KEY_MAP.put('ю', '9'); - CHAR_TO_KEY_MAP.put('я', '9'); - } - - private static RussianSmartDialMap instance; - - static RussianSmartDialMap getInstance() { - if (instance == null) { - instance = new RussianSmartDialMap(); - } - - return instance; - } - - private RussianSmartDialMap() {} - - @Override - Optional normalizeCharacter(char ch) { - ch = Character.toLowerCase(ch); - return isValidDialpadAlphabeticChar(ch) ? Optional.of(ch) : Optional.absent(); - } - - @Override - SimpleArrayMap getCharToKeyMap() { - return CHAR_TO_KEY_MAP; - } -} diff --git a/java/com/android/dialer/smartdial/SmartDialCursorLoader.java b/java/com/android/dialer/smartdial/SmartDialCursorLoader.java new file mode 100644 index 000000000..f6bc9325a --- /dev/null +++ b/java/com/android/dialer/smartdial/SmartDialCursorLoader.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2013 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.dialer.smartdial; + +import android.content.AsyncTaskLoader; +import android.content.Context; +import android.database.Cursor; +import android.database.MatrixCursor; +import com.android.contacts.common.list.PhoneNumberListAdapter.PhoneQuery; +import com.android.dialer.common.LogUtil; +import com.android.dialer.database.Database; +import com.android.dialer.database.DialerDatabaseHelper; +import com.android.dialer.database.DialerDatabaseHelper.ContactNumber; +import com.android.dialer.smartdial.util.SmartDialNameMatcher; +import com.android.dialer.util.PermissionsUtil; +import java.util.ArrayList; + +/** Implements a Loader class to asynchronously load SmartDial search results. */ +public class SmartDialCursorLoader extends AsyncTaskLoader { + + private static final String TAG = "SmartDialCursorLoader"; + private static final boolean DEBUG = false; + + private final Context mContext; + + private Cursor mCursor; + + private String mQuery; + private SmartDialNameMatcher mNameMatcher; + + private boolean mShowEmptyListForNullQuery = true; + + public SmartDialCursorLoader(Context context) { + super(context); + mContext = context; + } + + /** + * Configures the query string to be used to find SmartDial matches. + * + * @param query The query string user typed. + */ + public void configureQuery(String query) { + if (DEBUG) { + LogUtil.v(TAG, "Configure new query to be " + query); + } + mQuery = SmartDialNameMatcher.normalizeNumber(mContext, query); + + /** Constructs a name matcher object for matching names. */ + mNameMatcher = new SmartDialNameMatcher(mQuery); + mNameMatcher.setShouldMatchEmptyQuery(!mShowEmptyListForNullQuery); + } + + /** + * Queries the SmartDial database and loads results in background. + * + * @return Cursor of contacts that matches the SmartDial query. + */ + @Override + public Cursor loadInBackground() { + if (DEBUG) { + LogUtil.v(TAG, "Load in background " + mQuery); + } + + if (!PermissionsUtil.hasContactsReadPermissions(mContext)) { + return new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY); + } + + /** Loads results from the database helper. */ + final DialerDatabaseHelper dialerDatabaseHelper = + Database.get(mContext).getDatabaseHelper(mContext); + final ArrayList allMatches = + dialerDatabaseHelper.getLooseMatches(mQuery, mNameMatcher); + + if (DEBUG) { + LogUtil.v(TAG, "Loaded matches " + allMatches.size()); + } + + /** Constructs a cursor for the returned array of results. */ + final MatrixCursor cursor = new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY); + Object[] row = new Object[PhoneQuery.PROJECTION_PRIMARY.length]; + for (ContactNumber contact : allMatches) { + row[PhoneQuery.PHONE_ID] = contact.dataId; + row[PhoneQuery.PHONE_NUMBER] = contact.phoneNumber; + row[PhoneQuery.CONTACT_ID] = contact.id; + row[PhoneQuery.LOOKUP_KEY] = contact.lookupKey; + row[PhoneQuery.PHOTO_ID] = contact.photoId; + row[PhoneQuery.DISPLAY_NAME] = contact.displayName; + row[PhoneQuery.CARRIER_PRESENCE] = contact.carrierPresence; + cursor.addRow(row); + } + return cursor; + } + + @Override + public void deliverResult(Cursor cursor) { + if (isReset()) { + /** The Loader has been reset; ignore the result and invalidate the data. */ + releaseResources(cursor); + return; + } + + /** Hold a reference to the old data so it doesn't get garbage collected. */ + Cursor oldCursor = mCursor; + mCursor = cursor; + + if (isStarted()) { + /** If the Loader is in a started state, deliver the results to the client. */ + super.deliverResult(cursor); + } + + /** Invalidate the old data as we don't need it any more. */ + if (oldCursor != null && oldCursor != cursor) { + releaseResources(oldCursor); + } + } + + @Override + protected void onStartLoading() { + if (mCursor != null) { + /** Deliver any previously loaded data immediately. */ + deliverResult(mCursor); + } + if (mCursor == null) { + /** Force loads every time as our results change with queries. */ + forceLoad(); + } + } + + @Override + protected void onStopLoading() { + /** The Loader is in a stopped state, so we should attempt to cancel the current load. */ + cancelLoad(); + } + + @Override + protected void onReset() { + /** Ensure the loader has been stopped. */ + onStopLoading(); + + /** Release all previously saved query results. */ + if (mCursor != null) { + releaseResources(mCursor); + mCursor = null; + } + } + + @Override + public void onCanceled(Cursor cursor) { + super.onCanceled(cursor); + + /** The load has been canceled, so we should release the resources associated with 'data'. */ + releaseResources(cursor); + } + + private void releaseResources(Cursor cursor) { + if (cursor != null) { + cursor.close(); + } + } + + public void setShowEmptyListForNullQuery(boolean show) { + mShowEmptyListForNullQuery = show; + if (mNameMatcher != null) { + mNameMatcher.setShouldMatchEmptyQuery(!show); + } + } +} diff --git a/java/com/android/dialer/smartdial/SmartDialMap.java b/java/com/android/dialer/smartdial/SmartDialMap.java deleted file mode 100644 index bc5c9ea72..000000000 --- a/java/com/android/dialer/smartdial/SmartDialMap.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (C) 2016 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.dialer.smartdial; - -import android.support.v4.util.SimpleArrayMap; -import com.google.common.base.Optional; - -/** Definition for utilities that supports smart dial in different languages. */ -@SuppressWarnings("Guava") -abstract class SmartDialMap { - - /** - * Returns true if the provided character can be mapped to a key on the dialpad. - * - *

The provided character is expected to be a normalized character. See {@link - * SmartDialMap#normalizeCharacter(char)} for details. - */ - protected boolean isValidDialpadCharacter(char ch) { - return isValidDialpadAlphabeticChar(ch) || isValidDialpadNumericChar(ch); - } - - /** - * Returns true if the provided character is a letter and can be mapped to a key on the dialpad. - * - *

The provided character is expected to be a normalized character. See {@link - * SmartDialMap#normalizeCharacter(char)} for details. - */ - protected boolean isValidDialpadAlphabeticChar(char ch) { - return getCharToKeyMap().containsKey(ch); - } - - /** - * Returns true if the provided character is a digit, and can be mapped to a key on the dialpad. - */ - protected boolean isValidDialpadNumericChar(char ch) { - return '0' <= ch && ch <= '9'; - } - - /** - * Get the index of the key on the dialpad which the character corresponds to. - * - *

The provided character is expected to be a normalized character. See {@link - * SmartDialMap#normalizeCharacter(char)} for details. - * - *

An {@link Optional#absent()} is returned if the provided character can't be mapped to a key - * on the dialpad. - */ - protected Optional getDialpadIndex(char ch) { - if (isValidDialpadNumericChar(ch)) { - return Optional.of((byte) (ch - '0')); - } - - if (isValidDialpadAlphabeticChar(ch)) { - return Optional.of((byte) (getCharToKeyMap().get(ch) - '0')); - } - - return Optional.absent(); - } - - /** - * Get the actual numeric character on the dialpad which the character corresponds to. - * - *

The provided character is expected to be a normalized character. See {@link - * SmartDialMap#normalizeCharacter(char)} for details. - * - *

An {@link Optional#absent()} is returned if the provided character can't be mapped to a key - * on the dialpad. - */ - protected Optional getDialpadNumericCharacter(char ch) { - return isValidDialpadAlphabeticChar(ch) - ? Optional.of(getCharToKeyMap().get(ch)) - : Optional.absent(); - } - - /** - * Converts uppercase characters to lower case ones, and on a best effort basis, strips accents - * from accented characters. - * - *

An {@link Optional#absent()} is returned if the provided character can't be mapped to a key - * on the dialpad. - */ - abstract Optional normalizeCharacter(char ch); - - /** - * Returns a map in which each key is a normalized character and the corresponding value is a - * dialpad key. - */ - abstract SimpleArrayMap getCharToKeyMap(); -} diff --git a/java/com/android/dialer/smartdial/SmartDialMatchPosition.java b/java/com/android/dialer/smartdial/SmartDialMatchPosition.java deleted file mode 100644 index 8056ad723..000000000 --- a/java/com/android/dialer/smartdial/SmartDialMatchPosition.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2012 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.dialer.smartdial; - -import android.util.Log; -import java.util.ArrayList; - -/** - * Stores information about a range of characters matched in a display name The integers start and - * end indicate that the range start to end (exclusive) correspond to some characters in the query. - * Used to highlight certain parts of the contact's display name to indicate that those ranges - * matched the user's query. - */ -public class SmartDialMatchPosition { - - private static final String TAG = SmartDialMatchPosition.class.getSimpleName(); - - public int start; - public int end; - - public SmartDialMatchPosition(int start, int end) { - this.start = start; - this.end = end; - } - - /** - * Used by {@link SmartDialNameMatcher} to advance the positions of a match position found in a - * sub query. - * - * @param inList ArrayList of SmartDialMatchPositions to modify. - * @param toAdvance Offset to modify by. - */ - public static void advanceMatchPositions( - ArrayList inList, int toAdvance) { - for (int i = 0; i < inList.size(); i++) { - inList.get(i).advance(toAdvance); - } - } - - /** - * Used mainly for debug purposes. Displays contents of an ArrayList of SmartDialMatchPositions. - * - * @param list ArrayList of SmartDialMatchPositions to print out in a human readable fashion. - */ - public static void print(ArrayList list) { - for (int i = 0; i < list.size(); i++) { - SmartDialMatchPosition m = list.get(i); - Log.d(TAG, "[" + m.start + "," + m.end + "]"); - } - } - - private void advance(int toAdvance) { - this.start += toAdvance; - this.end += toAdvance; - } -} diff --git a/java/com/android/dialer/smartdial/SmartDialNameMatcher.java b/java/com/android/dialer/smartdial/SmartDialNameMatcher.java deleted file mode 100644 index 4e3e0cc3f..000000000 --- a/java/com/android/dialer/smartdial/SmartDialNameMatcher.java +++ /dev/null @@ -1,429 +0,0 @@ -/* - * Copyright (C) 2012 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.dialer.smartdial; - -import android.content.Context; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import com.android.dialer.smartdial.SmartDialPrefix.PhoneNumberTokens; -import java.util.ArrayList; - -/** - * {@link #SmartDialNameMatcher} contains utility functions to remove accents from accented - * characters and normalize a phone number. It also contains the matching logic that determines if a - * contact's display name matches a numeric query. The boolean variable {@link #ALLOW_INITIAL_MATCH} - * controls the behavior of the matching logic and determines whether we allow matches like 57 - - * (J)ohn (S)mith. - */ -public class SmartDialNameMatcher { - // Whether or not we allow matches like 57 - (J)ohn (S)mith - private static final boolean ALLOW_INITIAL_MATCH = true; - - // The maximum length of the initial we will match - typically set to 1 to minimize false - // positives - private static final int INITIAL_LENGTH_LIMIT = 1; - - private final ArrayList mMatchPositions = new ArrayList<>(); - private String mQuery; - - // Controls whether to treat an empty query as a match (with anything). - private boolean mShouldMatchEmptyQuery = false; - - public SmartDialNameMatcher(String query) { - mQuery = query; - } - - /** - * Strips a phone number of unnecessary characters (spaces, dashes, etc.) - * - * @param number Phone number we want to normalize - * @return Phone number consisting of digits from 0-9 - */ - public static String normalizeNumber(Context context, String number) { - return normalizeNumber(context, number, /* offset = */ 0); - } - - /** - * Strips a phone number of unnecessary characters (spaces, dashes, etc.) - * - * @param number Phone number we want to normalize - * @param offset Offset to start from - * @return Phone number consisting of digits from 0-9 - */ - public static String normalizeNumber(Context context, String number, int offset) { - final StringBuilder s = new StringBuilder(); - for (int i = offset; i < number.length(); i++) { - char ch = number.charAt(i); - if (CompositeSmartDialMap.isValidDialpadNumericChar(context, ch)) { - s.append(ch); - } - } - return s.toString(); - } - - /** - * Constructs empty highlight mask. Bit 0 at a position means there is no match, Bit 1 means there - * is a match and should be highlighted in the TextView. - * - * @param builder StringBuilder object - * @param length Length of the desired mask. - */ - private void constructEmptyMask(StringBuilder builder, int length) { - for (int i = 0; i < length; ++i) { - builder.append("0"); - } - } - - /** - * Replaces the 0-bit at a position with 1-bit, indicating that there is a match. - * - * @param builder StringBuilder object. - * @param matchPos Match Positions to mask as 1. - */ - private void replaceBitInMask(StringBuilder builder, SmartDialMatchPosition matchPos) { - for (int i = matchPos.start; i < matchPos.end; ++i) { - builder.replace(i, i + 1, "1"); - } - } - - /** - * Matches a phone number against a query. Let the test application overwrite the NANP setting. - * - * @param phoneNumber - Raw phone number - * @param query - Normalized query (only contains numbers from 0-9) - * @return {@literal null} if the number and the query don't match, a valid SmartDialMatchPosition - * with the matching positions otherwise - */ - @Nullable - public SmartDialMatchPosition matchesNumber(Context context, String phoneNumber, String query) { - if (TextUtils.isEmpty(phoneNumber)) { - return mShouldMatchEmptyQuery ? new SmartDialMatchPosition(0, 0) : null; - } - StringBuilder builder = new StringBuilder(); - constructEmptyMask(builder, phoneNumber.length()); - - // Try matching the number as is - SmartDialMatchPosition matchPos = - matchesNumberWithOffset(context, phoneNumber, query, /* offset = */ 0); - if (matchPos == null) { - PhoneNumberTokens phoneNumberTokens = SmartDialPrefix.parsePhoneNumber(context, phoneNumber); - - if (phoneNumberTokens.countryCodeOffset != 0) { - matchPos = - matchesNumberWithOffset( - context, phoneNumber, query, phoneNumberTokens.countryCodeOffset); - } - if (matchPos == null && phoneNumberTokens.nanpCodeOffset != 0) { - matchPos = - matchesNumberWithOffset(context, phoneNumber, query, phoneNumberTokens.nanpCodeOffset); - } - } - if (matchPos != null) { - replaceBitInMask(builder, matchPos); - } - return matchPos; - } - - /** - * Matches a phone number against the saved query, taking care of formatting characters and also - * taking into account country code prefixes and special NANP number treatment. - * - * @param phoneNumber - Raw phone number - * @return {@literal null} if the number and the query don't match, a valid SmartDialMatchPosition - * with the matching positions otherwise - */ - public SmartDialMatchPosition matchesNumber(Context context, String phoneNumber) { - return matchesNumber(context, phoneNumber, mQuery); - } - - /** - * Matches a phone number against a query, taking care of formatting characters - * - * @param phoneNumber - Raw phone number - * @param query - Normalized query (only contains numbers from 0-9) - * @param offset - The position in the number to start the match against (used to ignore leading - * prefixes/country codes) - * @return {@literal null} if the number and the query don't match, a valid SmartDialMatchPosition - * with the matching positions otherwise - */ - private SmartDialMatchPosition matchesNumberWithOffset( - Context context, String phoneNumber, String query, int offset) { - if (TextUtils.isEmpty(phoneNumber) || TextUtils.isEmpty(query)) { - return mShouldMatchEmptyQuery ? new SmartDialMatchPosition(offset, offset) : null; - } - int queryAt = 0; - int numberAt = offset; - for (int i = offset; i < phoneNumber.length(); i++) { - if (queryAt == query.length()) { - break; - } - char ch = phoneNumber.charAt(i); - if (CompositeSmartDialMap.isValidDialpadNumericChar(context, ch)) { - if (ch != query.charAt(queryAt)) { - return null; - } - queryAt++; - } else { - if (queryAt == 0) { - // Found a separator before any part of the query was matched, so advance the - // offset to avoid prematurely highlighting separators before the rest of the - // query. - // E.g. don't highlight the first '-' if we're matching 1-510-111-1111 with - // '510'. - // However, if the current offset is 0, just include the beginning separators - // anyway, otherwise the highlighting ends up looking weird. - // E.g. if we're matching (510)-111-1111 with '510', we should include the - // first '('. - if (offset != 0) { - offset++; - } - } - } - numberAt++; - } - return new SmartDialMatchPosition(0 + offset, numberAt); - } - - /** - * This function iterates through each token in the display name, trying to match the query to the - * numeric equivalent of the token. - * - *

A token is defined as a range in the display name delimited by characters that have no latin - * alphabet equivalents (e.g. spaces - ' ', periods - ',', underscores - '_' or chinese characters - * - '王'). Transliteration from non-latin characters to latin character will be done on a best - * effort basis - e.g. 'Ü' - 'u'. - * - *

For example, the display name "Phillips Thomas Jr" contains three tokens: "phillips", - * "thomas", and "jr". - * - *

A match must begin at the start of a token. For example, typing 846(Tho) would match - * "Phillips Thomas", but 466(hom) would not. - * - *

Also, a match can extend across tokens. For example, typing 37337(FredS) would match (Fred - * S)mith. - * - * @param displayName The normalized(no accented characters) display name we intend to match - * against. - * @param query The string of digits that we want to match the display name to. - * @param matchList An array list of {@link SmartDialMatchPosition}s that we add matched positions - * to. - * @return Returns true if a combination of the tokens in displayName match the query string - * contained in query. If the function returns true, matchList will contain an ArrayList of - * match positions (multiple matches correspond to initial matches). - */ - private boolean matchesCombination( - Context context, - String displayName, - String query, - ArrayList matchList) { - StringBuilder builder = new StringBuilder(); - constructEmptyMask(builder, displayName.length()); - final int nameLength = displayName.length(); - final int queryLength = query.length(); - - if (nameLength < queryLength) { - return false; - } - - if (queryLength == 0) { - return false; - } - - // The current character index in displayName - // E.g. 3 corresponds to 'd' in "Fred Smith" - int nameStart = 0; - - // The current character in the query we are trying to match the displayName against - int queryStart = 0; - - // The start position of the current token we are inspecting - int tokenStart = 0; - - // The number of non-alphabetic characters we've encountered so far in the current match. - // E.g. if we've currently matched 3733764849 to (Fred Smith W)illiam, then the - // seperatorCount should be 2. This allows us to correctly calculate offsets for the match - // positions - int seperatorCount = 0; - - ArrayList partial = new ArrayList(); - // Keep going until we reach the end of displayName - while (nameStart < nameLength && queryStart < queryLength) { - char ch = displayName.charAt(nameStart); - // Strip diacritics from accented characters if any - ch = CompositeSmartDialMap.normalizeCharacter(context, ch); - if (CompositeSmartDialMap.isValidDialpadCharacter(context, ch)) { - if (CompositeSmartDialMap.isValidDialpadAlphabeticChar(context, ch)) { - ch = CompositeSmartDialMap.getDialpadNumericCharacter(context, ch); - } - if (ch != query.charAt(queryStart)) { - // Failed to match the current character in the query. - - // Case 1: Failed to match the first character in the query. Skip to the next - // token since there is no chance of this token matching the query. - - // Case 2: Previous characters in the query matched, but the current character - // failed to match. This happened in the middle of a token. Skip to the next - // token since there is no chance of this token matching the query. - - // Case 3: Previous characters in the query matched, but the current character - // failed to match. This happened right at the start of the current token. In - // this case, we should restart the query and try again with the current token. - // Otherwise, we would fail to match a query like "964"(yog) against a name - // Yo-Yoghurt because the query match would fail on the 3rd character, and - // then skip to the end of the "Yoghurt" token. - - if (queryStart == 0 - || CompositeSmartDialMap.isValidDialpadCharacter( - context, - CompositeSmartDialMap.normalizeCharacter( - context, displayName.charAt(nameStart - 1)))) { - // skip to the next token, in the case of 1 or 2. - while (nameStart < nameLength - && CompositeSmartDialMap.isValidDialpadCharacter( - context, - CompositeSmartDialMap.normalizeCharacter( - context, displayName.charAt(nameStart)))) { - nameStart++; - } - nameStart++; - } - - // Restart the query and set the correct token position - queryStart = 0; - seperatorCount = 0; - tokenStart = nameStart; - } else { - if (queryStart == queryLength - 1) { - - // As much as possible, we prioritize a full token match over a sub token - // one so if we find a full token match, we can return right away - matchList.add( - new SmartDialMatchPosition(tokenStart, queryLength + tokenStart + seperatorCount)); - for (SmartDialMatchPosition match : matchList) { - replaceBitInMask(builder, match); - } - return true; - } else if (ALLOW_INITIAL_MATCH && queryStart < INITIAL_LENGTH_LIMIT) { - // we matched the first character. - // branch off and see if we can find another match with the remaining - // characters in the query string and the remaining tokens - // find the next separator in the query string - int j; - for (j = nameStart; j < nameLength; j++) { - if (!CompositeSmartDialMap.isValidDialpadCharacter( - context, - CompositeSmartDialMap.normalizeCharacter(context, displayName.charAt(j)))) { - break; - } - } - // this means there is at least one character left after the separator - if (j < nameLength - 1) { - final String remainder = displayName.substring(j + 1); - final ArrayList partialTemp = new ArrayList<>(); - if (matchesCombination( - context, remainder, query.substring(queryStart + 1), partialTemp)) { - - // store the list of possible match positions - SmartDialMatchPosition.advanceMatchPositions(partialTemp, j + 1); - partialTemp.add(0, new SmartDialMatchPosition(nameStart, nameStart + 1)); - // we found a partial token match, store the data in a - // temp buffer and return it if we end up not finding a full - // token match - partial = partialTemp; - } - } - } - nameStart++; - queryStart++; - // we matched the current character in the name against one in the query, - // continue and see if the rest of the characters match - } - } else { - // found a separator, we skip this character and continue to the next one - nameStart++; - if (queryStart == 0) { - // This means we found a separator before the start of a token, - // so we should increment the token's start position to reflect its true - // start position - tokenStart = nameStart; - } else { - // Otherwise this separator was found in the middle of a token being matched, - // so increase the separator count - seperatorCount++; - } - } - } - // if we have no complete match at this point, then we attempt to fall back to the partial - // token match(if any). If we don't allow initial matching (ALLOW_INITIAL_MATCH = false) - // then partial will always be empty. - if (!partial.isEmpty()) { - matchList.addAll(partial); - for (SmartDialMatchPosition match : matchList) { - replaceBitInMask(builder, match); - } - return true; - } - return false; - } - - /** - * This function iterates through each token in the display name, trying to match the query to the - * numeric equivalent of the token. - * - *

A token is defined as a range in the display name delimited by characters that have no latin - * alphabet equivalents (e.g. spaces - ' ', periods - ',', underscores - '_' or chinese characters - * - '王'). Transliteration from non-latin characters to latin character will be done on a best - * effort basis - e.g. 'Ü' - 'u'. - * - *

For example, the display name "Phillips Thomas Jr" contains three tokens: "phillips", - * "thomas", and "jr". - * - *

A match must begin at the start of a token. For example, typing 846(Tho) would match - * "Phillips Thomas", but 466(hom) would not. - * - *

Also, a match can extend across tokens. For example, typing 37337(FredS) would match (Fred - * S)mith. - * - * @param displayName The normalized(no accented characters) display name we intend to match - * against. - * @return Returns true if a combination of the tokens in displayName match the query string - * contained in query. If the function returns true, matchList will contain an ArrayList of - * match positions (multiple matches correspond to initial matches). - */ - public boolean matches(Context context, String displayName) { - mMatchPositions.clear(); - return matchesCombination(context, displayName, mQuery, mMatchPositions); - } - - public ArrayList getMatchPositions() { - // Return a clone of mMatchPositions so that the caller can use it without - // worrying about it changing - return new ArrayList<>(mMatchPositions); - } - - public String getQuery() { - return mQuery; - } - - public void setQuery(String query) { - mQuery = query; - } - - public void setShouldMatchEmptyQuery(boolean matches) { - mShouldMatchEmptyQuery = matches; - } -} diff --git a/java/com/android/dialer/smartdial/SmartDialPrefix.java b/java/com/android/dialer/smartdial/SmartDialPrefix.java deleted file mode 100644 index b9c1f8c11..000000000 --- a/java/com/android/dialer/smartdial/SmartDialPrefix.java +++ /dev/null @@ -1,601 +0,0 @@ -/* - * Copyright (C) 2013 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.dialer.smartdial; - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.support.annotation.VisibleForTesting; -import android.telephony.TelephonyManager; -import android.text.TextUtils; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Set; - -/** - * Smart Dial utility class to find prefixes of contacts. It contains both methods to find supported - * prefix combinations for contact names, and also methods to find supported prefix combinations for - * contacts' phone numbers. Each contact name is separated into several tokens, such as first name, - * middle name, family name etc. Each phone number is also separated into country code, NANP area - * code, and local number if such separation is possible. - */ -public class SmartDialPrefix { - - /** - * The number of starting and ending tokens in a contact's name considered for initials. For - * example, if both constants are set to 2, and a contact's name is "Albert Ben Charles Daniel Ed - * Foster", the first two tokens "Albert" "Ben", and last two tokens "Ed" "Foster" can be replaced - * by their initials in contact name matching. Users can look up this contact by combinations of - * his initials such as "AF" "BF" "EF" "ABF" "BEF" "ABEF" etc, but can not use combinations such - * as "CF" "DF" "ACF" "ADF" etc. - */ - private static final int LAST_TOKENS_FOR_INITIALS = 2; - - private static final int FIRST_TOKENS_FOR_INITIALS = 2; - - /** The country code of the user's sim card obtained by calling getSimCountryIso */ - private static final String PREF_USER_SIM_COUNTRY_CODE = - "DialtactsActivity_user_sim_country_code"; - - private static final String PREF_USER_SIM_COUNTRY_CODE_DEFAULT = null; - - private static String sUserSimCountryCode = PREF_USER_SIM_COUNTRY_CODE_DEFAULT; - /** Indicates whether user is in NANP regions. */ - private static boolean sUserInNanpRegion = false; - /** Set of country names that use NANP code. */ - private static Set sNanpCountries = null; - /** Set of supported country codes in front of the phone number. */ - private static Set sCountryCodes = null; - - private static boolean sNanpInitialized = false; - - /** Initializes the Nanp settings, and finds out whether user is in a NANP region. */ - public static void initializeNanpSettings(Context context) { - final TelephonyManager manager = - (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - if (manager != null) { - sUserSimCountryCode = manager.getSimCountryIso(); - } - - final SharedPreferences prefs = - PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); - - if (sUserSimCountryCode != null) { - /** Updates shared preferences with the latest country obtained from getSimCountryIso. */ - prefs.edit().putString(PREF_USER_SIM_COUNTRY_CODE, sUserSimCountryCode).apply(); - } else { - /** Uses previously stored country code if loading fails. */ - sUserSimCountryCode = - prefs.getString(PREF_USER_SIM_COUNTRY_CODE, PREF_USER_SIM_COUNTRY_CODE_DEFAULT); - } - /** Queries the NANP country list to find out whether user is in a NANP region. */ - sUserInNanpRegion = isCountryNanp(sUserSimCountryCode); - sNanpInitialized = true; - } - - /** - * Parses a contact's name into a list of separated tokens. - * - * @param contactName Contact's name stored in string. - * @return A list of name tokens, for example separated first names, last name, etc. - */ - public static ArrayList parseToIndexTokens(Context context, String contactName) { - final int length = contactName.length(); - final ArrayList result = new ArrayList<>(); - char c; - final StringBuilder currentIndexToken = new StringBuilder(); - /** - * Iterates through the whole name string. If the current character is a valid character, append - * it to the current token. If the current character is not a valid character, for example space - * " ", mark the current token as complete and add it to the list of tokens. - */ - for (int i = 0; i < length; i++) { - c = CompositeSmartDialMap.normalizeCharacter(context, contactName.charAt(i)); - if (CompositeSmartDialMap.isValidDialpadCharacter(context, c)) { - /** Converts a character into the number on dialpad that represents the character. */ - currentIndexToken.append(CompositeSmartDialMap.getDialpadIndex(context, c)); - } else { - if (currentIndexToken.length() != 0) { - result.add(currentIndexToken.toString()); - } - currentIndexToken.delete(0, currentIndexToken.length()); - } - } - - /** Adds the last token in case it has not been added. */ - if (currentIndexToken.length() != 0) { - result.add(currentIndexToken.toString()); - } - return result; - } - - /** - * Generates a list of strings that any prefix of any string in the list can be used to look up - * the contact's name. - * - * @param index The contact's name in string. - * @return A List of strings, whose prefix can be used to look up the contact. - */ - public static ArrayList generateNamePrefixes(Context context, String index) { - final ArrayList result = new ArrayList<>(); - - /** Parses the name into a list of tokens. */ - final ArrayList indexTokens = parseToIndexTokens(context, index); - - if (indexTokens.size() > 0) { - /** - * Adds the full token combinations to the list. For example, a contact with name "Albert Ben - * Ed Foster" can be looked up by any prefix of the following strings "Foster" "EdFoster" - * "BenEdFoster" and "AlbertBenEdFoster". This covers all cases of look up that contains only - * one token, and that spans multiple continuous tokens. - */ - final StringBuilder fullNameToken = new StringBuilder(); - for (int i = indexTokens.size() - 1; i >= 0; i--) { - fullNameToken.insert(0, indexTokens.get(i)); - result.add(fullNameToken.toString()); - } - - /** - * Adds initial combinations to the list, with the number of initials restricted by {@link - * #LAST_TOKENS_FOR_INITIALS} and {@link #FIRST_TOKENS_FOR_INITIALS}. For example, a contact - * with name "Albert Ben Ed Foster" can be looked up by any prefix of the following strings - * "EFoster" "BFoster" "BEFoster" "AFoster" "ABFoster" "AEFoster" and "ABEFoster". This covers - * all cases of initial lookup. - */ - ArrayList fullNames = new ArrayList<>(); - fullNames.add(indexTokens.get(indexTokens.size() - 1)); - final int recursiveNameStart = result.size(); - int recursiveNameEnd = result.size(); - String initial = ""; - for (int i = indexTokens.size() - 2; i >= 0; i--) { - if ((i >= indexTokens.size() - LAST_TOKENS_FOR_INITIALS) - || (i < FIRST_TOKENS_FOR_INITIALS)) { - initial = indexTokens.get(i).substring(0, 1); - - /** Recursively adds initial combinations to the list. */ - for (int j = 0; j < fullNames.size(); ++j) { - result.add(initial + fullNames.get(j)); - } - for (int j = recursiveNameStart; j < recursiveNameEnd; ++j) { - result.add(initial + result.get(j)); - } - recursiveNameEnd = result.size(); - final String currentFullName = fullNames.get(fullNames.size() - 1); - fullNames.add(indexTokens.get(i) + currentFullName); - } - } - } - - return result; - } - - /** - * Computes a list of number strings based on tokens of a given phone number. Any prefix of any - * string in the list can be used to look up the phone number. The list include the full phone - * number, the national number if there is a country code in the phone number, and the local - * number if there is an area code in the phone number following the NANP format. For example, if - * a user has phone number +41 71 394 8392, the list will contain 41713948392 and 713948392. Any - * prefix to either of the strings can be used to look up the phone number. If a user has a phone - * number +1 555-302-3029 (NANP format), the list will contain 15553023029, 5553023029, and - * 3023029. - * - * @param number String of user's phone number. - * @return A list of strings where any prefix of any entry can be used to look up the number. - */ - public static ArrayList parseToNumberTokens(Context context, String number) { - final ArrayList result = new ArrayList<>(); - if (!TextUtils.isEmpty(number)) { - /** Adds the full number to the list. */ - result.add(SmartDialNameMatcher.normalizeNumber(context, number)); - - final PhoneNumberTokens phoneNumberTokens = parsePhoneNumber(context, number); - if (phoneNumberTokens == null) { - return result; - } - - if (phoneNumberTokens.countryCodeOffset != 0) { - result.add( - SmartDialNameMatcher.normalizeNumber( - context, number, phoneNumberTokens.countryCodeOffset)); - } - - if (phoneNumberTokens.nanpCodeOffset != 0) { - result.add( - SmartDialNameMatcher.normalizeNumber( - context, number, phoneNumberTokens.nanpCodeOffset)); - } - } - return result; - } - - /** - * Parses a phone number to find out whether it has country code and NANP area code. - * - * @param number Raw phone number. - * @return a PhoneNumberToken instance with country code, NANP code information. - */ - public static PhoneNumberTokens parsePhoneNumber(Context context, String number) { - String countryCode = ""; - int countryCodeOffset = 0; - int nanpNumberOffset = 0; - - if (!TextUtils.isEmpty(number)) { - String normalizedNumber = SmartDialNameMatcher.normalizeNumber(context, number); - if (number.charAt(0) == '+') { - /** If the number starts with '+', tries to find valid country code. */ - for (int i = 1; i <= 1 + 3; i++) { - if (number.length() <= i) { - break; - } - countryCode = number.substring(1, i); - if (isValidCountryCode(countryCode)) { - countryCodeOffset = i; - break; - } - } - } else { - /** - * If the number does not start with '+', finds out whether it is in NANP format and has '1' - * preceding the number. - */ - if ((normalizedNumber.length() == 11) - && (normalizedNumber.charAt(0) == '1') - && (sUserInNanpRegion)) { - countryCode = "1"; - countryCodeOffset = number.indexOf(normalizedNumber.charAt(1)); - if (countryCodeOffset == -1) { - countryCodeOffset = 0; - } - } - } - - /** If user is in NANP region, finds out whether a number is in NANP format. */ - if (sUserInNanpRegion) { - String areaCode = ""; - if (countryCode.equals("") && normalizedNumber.length() == 10) { - /** - * if the number has no country code but fits the NANP format, extracts the NANP area - * code, and finds out offset of the local number. - */ - areaCode = normalizedNumber.substring(0, 3); - } else if (countryCode.equals("1") && normalizedNumber.length() == 11) { - /** - * If the number has country code '1', finds out area code and offset of the local number. - */ - areaCode = normalizedNumber.substring(1, 4); - } - if (!areaCode.equals("")) { - final int areaCodeIndex = number.indexOf(areaCode); - if (areaCodeIndex != -1) { - nanpNumberOffset = number.indexOf(areaCode) + 3; - } - } - } - } - return new PhoneNumberTokens(countryCode, countryCodeOffset, nanpNumberOffset); - } - - /** Checkes whether a country code is valid. */ - private static boolean isValidCountryCode(String countryCode) { - if (sCountryCodes == null) { - sCountryCodes = initCountryCodes(); - } - return sCountryCodes.contains(countryCode); - } - - private static Set initCountryCodes() { - final HashSet result = new HashSet(); - result.add("1"); - result.add("7"); - result.add("20"); - result.add("27"); - result.add("30"); - result.add("31"); - result.add("32"); - result.add("33"); - result.add("34"); - result.add("36"); - result.add("39"); - result.add("40"); - result.add("41"); - result.add("43"); - result.add("44"); - result.add("45"); - result.add("46"); - result.add("47"); - result.add("48"); - result.add("49"); - result.add("51"); - result.add("52"); - result.add("53"); - result.add("54"); - result.add("55"); - result.add("56"); - result.add("57"); - result.add("58"); - result.add("60"); - result.add("61"); - result.add("62"); - result.add("63"); - result.add("64"); - result.add("65"); - result.add("66"); - result.add("81"); - result.add("82"); - result.add("84"); - result.add("86"); - result.add("90"); - result.add("91"); - result.add("92"); - result.add("93"); - result.add("94"); - result.add("95"); - result.add("98"); - result.add("211"); - result.add("212"); - result.add("213"); - result.add("216"); - result.add("218"); - result.add("220"); - result.add("221"); - result.add("222"); - result.add("223"); - result.add("224"); - result.add("225"); - result.add("226"); - result.add("227"); - result.add("228"); - result.add("229"); - result.add("230"); - result.add("231"); - result.add("232"); - result.add("233"); - result.add("234"); - result.add("235"); - result.add("236"); - result.add("237"); - result.add("238"); - result.add("239"); - result.add("240"); - result.add("241"); - result.add("242"); - result.add("243"); - result.add("244"); - result.add("245"); - result.add("246"); - result.add("247"); - result.add("248"); - result.add("249"); - result.add("250"); - result.add("251"); - result.add("252"); - result.add("253"); - result.add("254"); - result.add("255"); - result.add("256"); - result.add("257"); - result.add("258"); - result.add("260"); - result.add("261"); - result.add("262"); - result.add("263"); - result.add("264"); - result.add("265"); - result.add("266"); - result.add("267"); - result.add("268"); - result.add("269"); - result.add("290"); - result.add("291"); - result.add("297"); - result.add("298"); - result.add("299"); - result.add("350"); - result.add("351"); - result.add("352"); - result.add("353"); - result.add("354"); - result.add("355"); - result.add("356"); - result.add("357"); - result.add("358"); - result.add("359"); - result.add("370"); - result.add("371"); - result.add("372"); - result.add("373"); - result.add("374"); - result.add("375"); - result.add("376"); - result.add("377"); - result.add("378"); - result.add("379"); - result.add("380"); - result.add("381"); - result.add("382"); - result.add("385"); - result.add("386"); - result.add("387"); - result.add("389"); - result.add("420"); - result.add("421"); - result.add("423"); - result.add("500"); - result.add("501"); - result.add("502"); - result.add("503"); - result.add("504"); - result.add("505"); - result.add("506"); - result.add("507"); - result.add("508"); - result.add("509"); - result.add("590"); - result.add("591"); - result.add("592"); - result.add("593"); - result.add("594"); - result.add("595"); - result.add("596"); - result.add("597"); - result.add("598"); - result.add("599"); - result.add("670"); - result.add("672"); - result.add("673"); - result.add("674"); - result.add("675"); - result.add("676"); - result.add("677"); - result.add("678"); - result.add("679"); - result.add("680"); - result.add("681"); - result.add("682"); - result.add("683"); - result.add("685"); - result.add("686"); - result.add("687"); - result.add("688"); - result.add("689"); - result.add("690"); - result.add("691"); - result.add("692"); - result.add("800"); - result.add("808"); - result.add("850"); - result.add("852"); - result.add("853"); - result.add("855"); - result.add("856"); - result.add("870"); - result.add("878"); - result.add("880"); - result.add("881"); - result.add("882"); - result.add("883"); - result.add("886"); - result.add("888"); - result.add("960"); - result.add("961"); - result.add("962"); - result.add("963"); - result.add("964"); - result.add("965"); - result.add("966"); - result.add("967"); - result.add("968"); - result.add("970"); - result.add("971"); - result.add("972"); - result.add("973"); - result.add("974"); - result.add("975"); - result.add("976"); - result.add("977"); - result.add("979"); - result.add("992"); - result.add("993"); - result.add("994"); - result.add("995"); - result.add("996"); - result.add("998"); - return result; - } - - /** - * Indicates whether the given country uses NANP numbers - * - * @param country ISO 3166 country code (case doesn't matter) - * @return True if country uses NANP numbers (e.g. US, Canada), false otherwise - * @see - * https://en.wikipedia.org/wiki/North_American_Numbering_Plan - */ - @VisibleForTesting - public static boolean isCountryNanp(String country) { - if (TextUtils.isEmpty(country)) { - return false; - } - if (sNanpCountries == null) { - sNanpCountries = initNanpCountries(); - } - return sNanpCountries.contains(country.toUpperCase()); - } - - private static Set initNanpCountries() { - final HashSet result = new HashSet(); - result.add("US"); // United States - result.add("CA"); // Canada - result.add("AS"); // American Samoa - result.add("AI"); // Anguilla - result.add("AG"); // Antigua and Barbuda - result.add("BS"); // Bahamas - result.add("BB"); // Barbados - result.add("BM"); // Bermuda - result.add("VG"); // British Virgin Islands - result.add("KY"); // Cayman Islands - result.add("DM"); // Dominica - result.add("DO"); // Dominican Republic - result.add("GD"); // Grenada - result.add("GU"); // Guam - result.add("JM"); // Jamaica - result.add("PR"); // Puerto Rico - result.add("MS"); // Montserrat - result.add("MP"); // Northern Mariana Islands - result.add("KN"); // Saint Kitts and Nevis - result.add("LC"); // Saint Lucia - result.add("VC"); // Saint Vincent and the Grenadines - result.add("TT"); // Trinidad and Tobago - result.add("TC"); // Turks and Caicos Islands - result.add("VI"); // U.S. Virgin Islands - return result; - } - - /** - * Returns whether the user is in a region that uses Nanp format based on the sim location. - * - * @return Whether user is in Nanp region. - */ - public static boolean getUserInNanpRegion() { - return sUserInNanpRegion; - } - - /** Explicitly setting the user Nanp to the given boolean */ - @VisibleForTesting - public static void setUserInNanpRegion(boolean userInNanpRegion) { - sUserInNanpRegion = userInNanpRegion; - } - - /** Class to record phone number parsing information. */ - public static class PhoneNumberTokens { - - /** Country code of the phone number. */ - final String countryCode; - - /** Offset of national number after the country code. */ - final int countryCodeOffset; - - /** Offset of local number after NANP area code. */ - final int nanpCodeOffset; - - public PhoneNumberTokens(String countryCode, int countryCodeOffset, int nanpCodeOffset) { - this.countryCode = countryCode; - this.countryCodeOffset = countryCodeOffset; - this.nanpCodeOffset = nanpCodeOffset; - } - } -} diff --git a/java/com/android/dialer/smartdial/UkrainianSmartDialMap.java b/java/com/android/dialer/smartdial/UkrainianSmartDialMap.java deleted file mode 100644 index 8ba53c45f..000000000 --- a/java/com/android/dialer/smartdial/UkrainianSmartDialMap.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2017 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.dialer.smartdial; - -import android.support.v4.util.SimpleArrayMap; -import com.google.common.base.Optional; - -/** A {@link SmartDialMap} for the Ukrainian alphabet. */ -final class UkrainianSmartDialMap extends SmartDialMap { - private static final SimpleArrayMap CHAR_TO_KEY_MAP = - new SimpleArrayMap<>(); - - // Reference: https://en.wikipedia.org/wiki/Ukrainian_alphabet - static { - CHAR_TO_KEY_MAP.put('а', '2'); - CHAR_TO_KEY_MAP.put('б', '2'); - CHAR_TO_KEY_MAP.put('в', '2'); - CHAR_TO_KEY_MAP.put('г', '2'); - CHAR_TO_KEY_MAP.put('ґ', '2'); - - CHAR_TO_KEY_MAP.put('д', '3'); - CHAR_TO_KEY_MAP.put('е', '3'); - CHAR_TO_KEY_MAP.put('є', '3'); - CHAR_TO_KEY_MAP.put('ж', '3'); - CHAR_TO_KEY_MAP.put('з', '3'); - - CHAR_TO_KEY_MAP.put('и', '4'); - CHAR_TO_KEY_MAP.put('і', '4'); - CHAR_TO_KEY_MAP.put('ї', '4'); - CHAR_TO_KEY_MAP.put('й', '4'); - CHAR_TO_KEY_MAP.put('к', '4'); - CHAR_TO_KEY_MAP.put('л', '4'); - - CHAR_TO_KEY_MAP.put('м', '5'); - CHAR_TO_KEY_MAP.put('н', '5'); - CHAR_TO_KEY_MAP.put('о', '5'); - CHAR_TO_KEY_MAP.put('п', '5'); - - CHAR_TO_KEY_MAP.put('р', '6'); - CHAR_TO_KEY_MAP.put('с', '6'); - CHAR_TO_KEY_MAP.put('т', '6'); - CHAR_TO_KEY_MAP.put('у', '6'); - - CHAR_TO_KEY_MAP.put('ф', '7'); - CHAR_TO_KEY_MAP.put('х', '7'); - CHAR_TO_KEY_MAP.put('ц', '7'); - CHAR_TO_KEY_MAP.put('ч', '7'); - - CHAR_TO_KEY_MAP.put('ш', '8'); - CHAR_TO_KEY_MAP.put('щ', '8'); - - CHAR_TO_KEY_MAP.put('ь', '9'); - CHAR_TO_KEY_MAP.put('ю', '9'); - CHAR_TO_KEY_MAP.put('я', '9'); - } - - private static UkrainianSmartDialMap instance; - - static UkrainianSmartDialMap getInstance() { - if (instance == null) { - instance = new UkrainianSmartDialMap(); - } - - return instance; - } - - private UkrainianSmartDialMap() {} - - @Override - Optional normalizeCharacter(char ch) { - ch = Character.toLowerCase(ch); - return isValidDialpadAlphabeticChar(ch) ? Optional.of(ch) : Optional.absent(); - } - - @Override - SimpleArrayMap getCharToKeyMap() { - return CHAR_TO_KEY_MAP; - } -} diff --git a/java/com/android/dialer/smartdial/map/BulgarianSmartDialMap.java b/java/com/android/dialer/smartdial/map/BulgarianSmartDialMap.java new file mode 100644 index 000000000..cbe6afa97 --- /dev/null +++ b/java/com/android/dialer/smartdial/map/BulgarianSmartDialMap.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2017 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.dialer.smartdial.map; + +import android.support.v4.util.SimpleArrayMap; +import com.google.common.base.Optional; + +/** A {@link SmartDialMap} for the Bulgarian alphabet. */ +@SuppressWarnings("Guava") +final class BulgarianSmartDialMap extends SmartDialMap { + private static final SimpleArrayMap CHAR_TO_KEY_MAP = + new SimpleArrayMap<>(); + + // Reference: https://en.wikipedia.org/wiki/Bulgarian_alphabet + static { + CHAR_TO_KEY_MAP.put('а', '2'); + CHAR_TO_KEY_MAP.put('б', '2'); + CHAR_TO_KEY_MAP.put('в', '2'); + CHAR_TO_KEY_MAP.put('г', '2'); + + CHAR_TO_KEY_MAP.put('д', '3'); + CHAR_TO_KEY_MAP.put('е', '3'); + CHAR_TO_KEY_MAP.put('ж', '3'); + CHAR_TO_KEY_MAP.put('з', '3'); + + CHAR_TO_KEY_MAP.put('и', '4'); + CHAR_TO_KEY_MAP.put('й', '4'); + CHAR_TO_KEY_MAP.put('к', '4'); + CHAR_TO_KEY_MAP.put('л', '4'); + + CHAR_TO_KEY_MAP.put('м', '5'); + CHAR_TO_KEY_MAP.put('н', '5'); + CHAR_TO_KEY_MAP.put('о', '5'); + + CHAR_TO_KEY_MAP.put('п', '6'); + CHAR_TO_KEY_MAP.put('р', '6'); + CHAR_TO_KEY_MAP.put('с', '6'); + + CHAR_TO_KEY_MAP.put('т', '7'); + CHAR_TO_KEY_MAP.put('у', '7'); + CHAR_TO_KEY_MAP.put('ф', '7'); + CHAR_TO_KEY_MAP.put('х', '7'); + + CHAR_TO_KEY_MAP.put('ц', '8'); + CHAR_TO_KEY_MAP.put('ч', '8'); + CHAR_TO_KEY_MAP.put('ш', '8'); + CHAR_TO_KEY_MAP.put('щ', '8'); + + CHAR_TO_KEY_MAP.put('ъ', '9'); + CHAR_TO_KEY_MAP.put('ь', '9'); + CHAR_TO_KEY_MAP.put('ю', '9'); + CHAR_TO_KEY_MAP.put('я', '9'); + } + + private static BulgarianSmartDialMap instance; + + static BulgarianSmartDialMap getInstance() { + if (instance == null) { + instance = new BulgarianSmartDialMap(); + } + + return instance; + } + + private BulgarianSmartDialMap() {} + + @Override + Optional normalizeCharacter(char ch) { + ch = Character.toLowerCase(ch); + return isValidDialpadAlphabeticChar(ch) ? Optional.of(ch) : Optional.absent(); + } + + @Override + SimpleArrayMap getCharToKeyMap() { + return CHAR_TO_KEY_MAP; + } +} diff --git a/java/com/android/dialer/smartdial/map/CompositeSmartDialMap.java b/java/com/android/dialer/smartdial/map/CompositeSmartDialMap.java new file mode 100644 index 000000000..df32d4ce7 --- /dev/null +++ b/java/com/android/dialer/smartdial/map/CompositeSmartDialMap.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2017 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.dialer.smartdial.map; + +import android.content.Context; +import android.support.annotation.VisibleForTesting; +import android.support.v4.util.SimpleArrayMap; +import com.android.dialer.compat.CompatUtils; +import com.android.dialer.configprovider.ConfigProviderBindings; +import com.google.common.base.Optional; + +/** + * A utility class that combines the functionality of two implementations of {@link SmartDialMap} so + * that we support smart dial for dual alphabets. + * + *

Of the two implementations of {@link SmartDialMap}, the default one always takes precedence. + * The second one is consulted only when the default one is unable to provide a valid result. + * + *

Note that the second implementation can be absent if it is not defined for the system's 1st + * language preference. + */ +@SuppressWarnings("Guava") +public class CompositeSmartDialMap { + @VisibleForTesting + public static final String FLAG_ENABLE_DUAL_ALPHABETS = "enable_dual_alphabets_on_t9"; + + private static final SmartDialMap DEFAULT_MAP = LatinSmartDialMap.getInstance(); + + // A map in which each key is an ISO 639-2 language code and the corresponding value is a + // SmartDialMap + private static final SimpleArrayMap EXTRA_MAPS = new SimpleArrayMap<>(); + + static { + EXTRA_MAPS.put("bul", BulgarianSmartDialMap.getInstance()); + EXTRA_MAPS.put("rus", RussianSmartDialMap.getInstance()); + EXTRA_MAPS.put("ukr", UkrainianSmartDialMap.getInstance()); + } + + private CompositeSmartDialMap() {} + + /** + * Returns true if the provided character can be mapped to a key on the dialpad. + * + *

The provided character is expected to be a normalized character. See {@link + * SmartDialMap#normalizeCharacter(char)} for details. + */ + public static boolean isValidDialpadCharacter(Context context, char ch) { + if (DEFAULT_MAP.isValidDialpadCharacter(ch)) { + return true; + } + + Optional extraMap = getExtraMap(context); + return extraMap.isPresent() && extraMap.get().isValidDialpadCharacter(ch); + } + + /** + * Returns true if the provided character is a letter, and can be mapped to a key on the dialpad. + * + *

The provided character is expected to be a normalized character. See {@link + * SmartDialMap#normalizeCharacter(char)} for details. + */ + public static boolean isValidDialpadAlphabeticChar(Context context, char ch) { + if (DEFAULT_MAP.isValidDialpadAlphabeticChar(ch)) { + return true; + } + + Optional extraMap = getExtraMap(context); + return extraMap.isPresent() && extraMap.get().isValidDialpadAlphabeticChar(ch); + } + + /** + * Returns true if the provided character is a digit, and can be mapped to a key on the dialpad. + */ + public static boolean isValidDialpadNumericChar(Context context, char ch) { + if (DEFAULT_MAP.isValidDialpadNumericChar(ch)) { + return true; + } + + Optional extraMap = getExtraMap(context); + return extraMap.isPresent() && extraMap.get().isValidDialpadNumericChar(ch); + } + + /** + * Get the index of the key on the dialpad which the character corresponds to. + * + *

The provided character is expected to be a normalized character. See {@link + * SmartDialMap#normalizeCharacter(char)} for details. + * + *

If the provided character can't be mapped to a key on the dialpad, return -1. + */ + public static byte getDialpadIndex(Context context, char ch) { + Optional dialpadIndex = DEFAULT_MAP.getDialpadIndex(ch); + if (dialpadIndex.isPresent()) { + return dialpadIndex.get(); + } + + Optional extraMap = getExtraMap(context); + if (extraMap.isPresent()) { + dialpadIndex = extraMap.get().getDialpadIndex(ch); + } + + return dialpadIndex.isPresent() ? dialpadIndex.get() : -1; + } + + /** + * Get the actual numeric character on the dialpad which the character corresponds to. + * + *

The provided character is expected to be a normalized character. See {@link + * SmartDialMap#normalizeCharacter(char)} for details. + * + *

If the provided character can't be mapped to a key on the dialpad, return the character. + */ + public static char getDialpadNumericCharacter(Context context, char ch) { + Optional dialpadNumericChar = DEFAULT_MAP.getDialpadNumericCharacter(ch); + if (dialpadNumericChar.isPresent()) { + return dialpadNumericChar.get(); + } + + Optional extraMap = getExtraMap(context); + if (extraMap.isPresent()) { + dialpadNumericChar = extraMap.get().getDialpadNumericCharacter(ch); + } + + return dialpadNumericChar.isPresent() ? dialpadNumericChar.get() : ch; + } + + /** + * Converts uppercase characters to lower case ones, and on a best effort basis, strips accents + * from accented characters. + * + *

If the provided character can't be mapped to a key on the dialpad, return the character. + */ + public static char normalizeCharacter(Context context, char ch) { + Optional normalizedChar = DEFAULT_MAP.normalizeCharacter(ch); + if (normalizedChar.isPresent()) { + return normalizedChar.get(); + } + + Optional extraMap = getExtraMap(context); + if (extraMap.isPresent()) { + normalizedChar = extraMap.get().normalizeCharacter(ch); + } + + return normalizedChar.isPresent() ? normalizedChar.get() : ch; + } + + @VisibleForTesting + static Optional getExtraMap(Context context) { + if (!ConfigProviderBindings.get(context).getBoolean(FLAG_ENABLE_DUAL_ALPHABETS, false)) { + return Optional.absent(); + } + + String languageCode = CompatUtils.getLocale(context).getISO3Language(); + return EXTRA_MAPS.containsKey(languageCode) + ? Optional.of(EXTRA_MAPS.get(languageCode)) + : Optional.absent(); + } +} diff --git a/java/com/android/dialer/smartdial/map/LatinSmartDialMap.java b/java/com/android/dialer/smartdial/map/LatinSmartDialMap.java new file mode 100644 index 000000000..052af05c0 --- /dev/null +++ b/java/com/android/dialer/smartdial/map/LatinSmartDialMap.java @@ -0,0 +1,785 @@ +/* + * Copyright (C) 2016 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.dialer.smartdial.map; + +import android.support.v4.util.SimpleArrayMap; +import com.google.common.base.Optional; + +/** A {@link SmartDialMap} for the Latin alphabet, which is for T9 dialpad searching. */ +@SuppressWarnings("Guava") +final class LatinSmartDialMap extends SmartDialMap { + private static final SimpleArrayMap CHAR_TO_KEY_MAP = + new SimpleArrayMap<>(); + + static { + CHAR_TO_KEY_MAP.put('a', '2'); + CHAR_TO_KEY_MAP.put('b', '2'); + CHAR_TO_KEY_MAP.put('c', '2'); + + CHAR_TO_KEY_MAP.put('d', '3'); + CHAR_TO_KEY_MAP.put('e', '3'); + CHAR_TO_KEY_MAP.put('f', '3'); + + CHAR_TO_KEY_MAP.put('g', '4'); + CHAR_TO_KEY_MAP.put('h', '4'); + CHAR_TO_KEY_MAP.put('i', '4'); + + CHAR_TO_KEY_MAP.put('j', '5'); + CHAR_TO_KEY_MAP.put('k', '5'); + CHAR_TO_KEY_MAP.put('l', '5'); + + CHAR_TO_KEY_MAP.put('m', '6'); + CHAR_TO_KEY_MAP.put('n', '6'); + CHAR_TO_KEY_MAP.put('o', '6'); + + CHAR_TO_KEY_MAP.put('p', '7'); + CHAR_TO_KEY_MAP.put('q', '7'); + CHAR_TO_KEY_MAP.put('r', '7'); + CHAR_TO_KEY_MAP.put('s', '7'); + + CHAR_TO_KEY_MAP.put('t', '8'); + CHAR_TO_KEY_MAP.put('u', '8'); + CHAR_TO_KEY_MAP.put('v', '8'); + + CHAR_TO_KEY_MAP.put('w', '9'); + CHAR_TO_KEY_MAP.put('x', '9'); + CHAR_TO_KEY_MAP.put('y', '9'); + CHAR_TO_KEY_MAP.put('z', '9'); + } + + private static LatinSmartDialMap instance; + + static LatinSmartDialMap getInstance() { + if (instance == null) { + instance = new LatinSmartDialMap(); + } + + return instance; + } + + private LatinSmartDialMap() {} + + /* + * The switch statement in this function was generated using the python code: + * from unidecode import unidecode + * for i in range(192, 564): + * char = unichr(i) + * decoded = unidecode(char) + * # Unicode characters that decompose into multiple characters i.e. + * # into ss are not supported for now + * if (len(decoded) == 1 and decoded.isalpha()): + * print "case '" + char + "': return Optional.of('" + unidecode(char) + "');" + * + * This gives us a way to map characters containing accents/diacritics to their + * alphabetic equivalents. The unidecode library can be found at: + * http://pypi.python.org/pypi/Unidecode/0.04.1 + * + * Also remaps all upper case latin characters to their lower case equivalents. + */ + @Override + Optional normalizeCharacter(char ch) { + if (isValidDialpadAlphabeticChar(ch)) { + return Optional.of(ch); + } + + switch (ch) { + case 'À': + return Optional.of('a'); + case 'Á': + return Optional.of('a'); + case 'Â': + return Optional.of('a'); + case 'Ã': + return Optional.of('a'); + case 'Ä': + return Optional.of('a'); + case 'Å': + return Optional.of('a'); + case 'Ç': + return Optional.of('c'); + case 'È': + return Optional.of('e'); + case 'É': + return Optional.of('e'); + case 'Ê': + return Optional.of('e'); + case 'Ë': + return Optional.of('e'); + case 'Ì': + return Optional.of('i'); + case 'Í': + return Optional.of('i'); + case 'Î': + return Optional.of('i'); + case 'Ï': + return Optional.of('i'); + case 'Ð': + return Optional.of('d'); + case 'Ñ': + return Optional.of('n'); + case 'Ò': + return Optional.of('o'); + case 'Ó': + return Optional.of('o'); + case 'Ô': + return Optional.of('o'); + case 'Õ': + return Optional.of('o'); + case 'Ö': + return Optional.of('o'); + case '×': + return Optional.of('x'); + case 'Ø': + return Optional.of('o'); + case 'Ù': + return Optional.of('u'); + case 'Ú': + return Optional.of('u'); + case 'Û': + return Optional.of('u'); + case 'Ü': + return Optional.of('u'); + case 'Ý': + return Optional.of('u'); + case 'à': + return Optional.of('a'); + case 'á': + return Optional.of('a'); + case 'â': + return Optional.of('a'); + case 'ã': + return Optional.of('a'); + case 'ä': + return Optional.of('a'); + case 'å': + return Optional.of('a'); + case 'ç': + return Optional.of('c'); + case 'è': + return Optional.of('e'); + case 'é': + return Optional.of('e'); + case 'ê': + return Optional.of('e'); + case 'ë': + return Optional.of('e'); + case 'ì': + return Optional.of('i'); + case 'í': + return Optional.of('i'); + case 'î': + return Optional.of('i'); + case 'ï': + return Optional.of('i'); + case 'ð': + return Optional.of('d'); + case 'ñ': + return Optional.of('n'); + case 'ò': + return Optional.of('o'); + case 'ó': + return Optional.of('o'); + case 'ô': + return Optional.of('o'); + case 'õ': + return Optional.of('o'); + case 'ö': + return Optional.of('o'); + case 'ø': + return Optional.of('o'); + case 'ù': + return Optional.of('u'); + case 'ú': + return Optional.of('u'); + case 'û': + return Optional.of('u'); + case 'ü': + return Optional.of('u'); + case 'ý': + return Optional.of('y'); + case 'ÿ': + return Optional.of('y'); + case 'Ā': + return Optional.of('a'); + case 'ā': + return Optional.of('a'); + case 'Ă': + return Optional.of('a'); + case 'ă': + return Optional.of('a'); + case 'Ą': + return Optional.of('a'); + case 'ą': + return Optional.of('a'); + case 'Ć': + return Optional.of('c'); + case 'ć': + return Optional.of('c'); + case 'Ĉ': + return Optional.of('c'); + case 'ĉ': + return Optional.of('c'); + case 'Ċ': + return Optional.of('c'); + case 'ċ': + return Optional.of('c'); + case 'Č': + return Optional.of('c'); + case 'č': + return Optional.of('c'); + case 'Ď': + return Optional.of('d'); + case 'ď': + return Optional.of('d'); + case 'Đ': + return Optional.of('d'); + case 'đ': + return Optional.of('d'); + case 'Ē': + return Optional.of('e'); + case 'ē': + return Optional.of('e'); + case 'Ĕ': + return Optional.of('e'); + case 'ĕ': + return Optional.of('e'); + case 'Ė': + return Optional.of('e'); + case 'ė': + return Optional.of('e'); + case 'Ę': + return Optional.of('e'); + case 'ę': + return Optional.of('e'); + case 'Ě': + return Optional.of('e'); + case 'ě': + return Optional.of('e'); + case 'Ĝ': + return Optional.of('g'); + case 'ĝ': + return Optional.of('g'); + case 'Ğ': + return Optional.of('g'); + case 'ğ': + return Optional.of('g'); + case 'Ġ': + return Optional.of('g'); + case 'ġ': + return Optional.of('g'); + case 'Ģ': + return Optional.of('g'); + case 'ģ': + return Optional.of('g'); + case 'Ĥ': + return Optional.of('h'); + case 'ĥ': + return Optional.of('h'); + case 'Ħ': + return Optional.of('h'); + case 'ħ': + return Optional.of('h'); + case 'Ĩ': + return Optional.of('i'); + case 'ĩ': + return Optional.of('i'); + case 'Ī': + return Optional.of('i'); + case 'ī': + return Optional.of('i'); + case 'Ĭ': + return Optional.of('i'); + case 'ĭ': + return Optional.of('i'); + case 'Į': + return Optional.of('i'); + case 'į': + return Optional.of('i'); + case 'İ': + return Optional.of('i'); + case 'ı': + return Optional.of('i'); + case 'Ĵ': + return Optional.of('j'); + case 'ĵ': + return Optional.of('j'); + case 'Ķ': + return Optional.of('k'); + case 'ķ': + return Optional.of('k'); + case 'ĸ': + return Optional.of('k'); + case 'Ĺ': + return Optional.of('l'); + case 'ĺ': + return Optional.of('l'); + case 'Ļ': + return Optional.of('l'); + case 'ļ': + return Optional.of('l'); + case 'Ľ': + return Optional.of('l'); + case 'ľ': + return Optional.of('l'); + case 'Ŀ': + return Optional.of('l'); + case 'ŀ': + return Optional.of('l'); + case 'Ł': + return Optional.of('l'); + case 'ł': + return Optional.of('l'); + case 'Ń': + return Optional.of('n'); + case 'ń': + return Optional.of('n'); + case 'Ņ': + return Optional.of('n'); + case 'ņ': + return Optional.of('n'); + case 'Ň': + return Optional.of('n'); + case 'ň': + return Optional.of('n'); + case 'Ō': + return Optional.of('o'); + case 'ō': + return Optional.of('o'); + case 'Ŏ': + return Optional.of('o'); + case 'ŏ': + return Optional.of('o'); + case 'Ő': + return Optional.of('o'); + case 'ő': + return Optional.of('o'); + case 'Ŕ': + return Optional.of('r'); + case 'ŕ': + return Optional.of('r'); + case 'Ŗ': + return Optional.of('r'); + case 'ŗ': + return Optional.of('r'); + case 'Ř': + return Optional.of('r'); + case 'ř': + return Optional.of('r'); + case 'Ś': + return Optional.of('s'); + case 'ś': + return Optional.of('s'); + case 'Ŝ': + return Optional.of('s'); + case 'ŝ': + return Optional.of('s'); + case 'Ş': + return Optional.of('s'); + case 'ş': + return Optional.of('s'); + case 'Š': + return Optional.of('s'); + case 'š': + return Optional.of('s'); + case 'Ţ': + return Optional.of('t'); + case 'ţ': + return Optional.of('t'); + case 'Ť': + return Optional.of('t'); + case 'ť': + return Optional.of('t'); + case 'Ŧ': + return Optional.of('t'); + case 'ŧ': + return Optional.of('t'); + case 'Ũ': + return Optional.of('u'); + case 'ũ': + return Optional.of('u'); + case 'Ū': + return Optional.of('u'); + case 'ū': + return Optional.of('u'); + case 'Ŭ': + return Optional.of('u'); + case 'ŭ': + return Optional.of('u'); + case 'Ů': + return Optional.of('u'); + case 'ů': + return Optional.of('u'); + case 'Ű': + return Optional.of('u'); + case 'ű': + return Optional.of('u'); + case 'Ų': + return Optional.of('u'); + case 'ų': + return Optional.of('u'); + case 'Ŵ': + return Optional.of('w'); + case 'ŵ': + return Optional.of('w'); + case 'Ŷ': + return Optional.of('y'); + case 'ŷ': + return Optional.of('y'); + case 'Ÿ': + return Optional.of('y'); + case 'Ź': + return Optional.of('z'); + case 'ź': + return Optional.of('z'); + case 'Ż': + return Optional.of('z'); + case 'ż': + return Optional.of('z'); + case 'Ž': + return Optional.of('z'); + case 'ž': + return Optional.of('z'); + case 'ſ': + return Optional.of('s'); + case 'ƀ': + return Optional.of('b'); + case 'Ɓ': + return Optional.of('b'); + case 'Ƃ': + return Optional.of('b'); + case 'ƃ': + return Optional.of('b'); + case 'Ɔ': + return Optional.of('o'); + case 'Ƈ': + return Optional.of('c'); + case 'ƈ': + return Optional.of('c'); + case 'Ɖ': + return Optional.of('d'); + case 'Ɗ': + return Optional.of('d'); + case 'Ƌ': + return Optional.of('d'); + case 'ƌ': + return Optional.of('d'); + case 'ƍ': + return Optional.of('d'); + case 'Ɛ': + return Optional.of('e'); + case 'Ƒ': + return Optional.of('f'); + case 'ƒ': + return Optional.of('f'); + case 'Ɠ': + return Optional.of('g'); + case 'Ɣ': + return Optional.of('g'); + case 'Ɩ': + return Optional.of('i'); + case 'Ɨ': + return Optional.of('i'); + case 'Ƙ': + return Optional.of('k'); + case 'ƙ': + return Optional.of('k'); + case 'ƚ': + return Optional.of('l'); + case 'ƛ': + return Optional.of('l'); + case 'Ɯ': + return Optional.of('w'); + case 'Ɲ': + return Optional.of('n'); + case 'ƞ': + return Optional.of('n'); + case 'Ɵ': + return Optional.of('o'); + case 'Ơ': + return Optional.of('o'); + case 'ơ': + return Optional.of('o'); + case 'Ƥ': + return Optional.of('p'); + case 'ƥ': + return Optional.of('p'); + case 'ƫ': + return Optional.of('t'); + case 'Ƭ': + return Optional.of('t'); + case 'ƭ': + return Optional.of('t'); + case 'Ʈ': + return Optional.of('t'); + case 'Ư': + return Optional.of('u'); + case 'ư': + return Optional.of('u'); + case 'Ʊ': + return Optional.of('y'); + case 'Ʋ': + return Optional.of('v'); + case 'Ƴ': + return Optional.of('y'); + case 'ƴ': + return Optional.of('y'); + case 'Ƶ': + return Optional.of('z'); + case 'ƶ': + return Optional.of('z'); + case 'ƿ': + return Optional.of('w'); + case 'Ǎ': + return Optional.of('a'); + case 'ǎ': + return Optional.of('a'); + case 'Ǐ': + return Optional.of('i'); + case 'ǐ': + return Optional.of('i'); + case 'Ǒ': + return Optional.of('o'); + case 'ǒ': + return Optional.of('o'); + case 'Ǔ': + return Optional.of('u'); + case 'ǔ': + return Optional.of('u'); + case 'Ǖ': + return Optional.of('u'); + case 'ǖ': + return Optional.of('u'); + case 'Ǘ': + return Optional.of('u'); + case 'ǘ': + return Optional.of('u'); + case 'Ǚ': + return Optional.of('u'); + case 'ǚ': + return Optional.of('u'); + case 'Ǜ': + return Optional.of('u'); + case 'ǜ': + return Optional.of('u'); + case 'Ǟ': + return Optional.of('a'); + case 'ǟ': + return Optional.of('a'); + case 'Ǡ': + return Optional.of('a'); + case 'ǡ': + return Optional.of('a'); + case 'Ǥ': + return Optional.of('g'); + case 'ǥ': + return Optional.of('g'); + case 'Ǧ': + return Optional.of('g'); + case 'ǧ': + return Optional.of('g'); + case 'Ǩ': + return Optional.of('k'); + case 'ǩ': + return Optional.of('k'); + case 'Ǫ': + return Optional.of('o'); + case 'ǫ': + return Optional.of('o'); + case 'Ǭ': + return Optional.of('o'); + case 'ǭ': + return Optional.of('o'); + case 'ǰ': + return Optional.of('j'); + case 'Dz': + return Optional.of('d'); + case 'Ǵ': + return Optional.of('g'); + case 'ǵ': + return Optional.of('g'); + case 'Ƿ': + return Optional.of('w'); + case 'Ǹ': + return Optional.of('n'); + case 'ǹ': + return Optional.of('n'); + case 'Ǻ': + return Optional.of('a'); + case 'ǻ': + return Optional.of('a'); + case 'Ǿ': + return Optional.of('o'); + case 'ǿ': + return Optional.of('o'); + case 'Ȁ': + return Optional.of('a'); + case 'ȁ': + return Optional.of('a'); + case 'Ȃ': + return Optional.of('a'); + case 'ȃ': + return Optional.of('a'); + case 'Ȅ': + return Optional.of('e'); + case 'ȅ': + return Optional.of('e'); + case 'Ȇ': + return Optional.of('e'); + case 'ȇ': + return Optional.of('e'); + case 'Ȉ': + return Optional.of('i'); + case 'ȉ': + return Optional.of('i'); + case 'Ȋ': + return Optional.of('i'); + case 'ȋ': + return Optional.of('i'); + case 'Ȍ': + return Optional.of('o'); + case 'ȍ': + return Optional.of('o'); + case 'Ȏ': + return Optional.of('o'); + case 'ȏ': + return Optional.of('o'); + case 'Ȑ': + return Optional.of('r'); + case 'ȑ': + return Optional.of('r'); + case 'Ȓ': + return Optional.of('r'); + case 'ȓ': + return Optional.of('r'); + case 'Ȕ': + return Optional.of('u'); + case 'ȕ': + return Optional.of('u'); + case 'Ȗ': + return Optional.of('u'); + case 'ȗ': + return Optional.of('u'); + case 'Ș': + return Optional.of('s'); + case 'ș': + return Optional.of('s'); + case 'Ț': + return Optional.of('t'); + case 'ț': + return Optional.of('t'); + case 'Ȝ': + return Optional.of('y'); + case 'ȝ': + return Optional.of('y'); + case 'Ȟ': + return Optional.of('h'); + case 'ȟ': + return Optional.of('h'); + case 'Ȥ': + return Optional.of('z'); + case 'ȥ': + return Optional.of('z'); + case 'Ȧ': + return Optional.of('a'); + case 'ȧ': + return Optional.of('a'); + case 'Ȩ': + return Optional.of('e'); + case 'ȩ': + return Optional.of('e'); + case 'Ȫ': + return Optional.of('o'); + case 'ȫ': + return Optional.of('o'); + case 'Ȭ': + return Optional.of('o'); + case 'ȭ': + return Optional.of('o'); + case 'Ȯ': + return Optional.of('o'); + case 'ȯ': + return Optional.of('o'); + case 'Ȱ': + return Optional.of('o'); + case 'ȱ': + return Optional.of('o'); + case 'Ȳ': + return Optional.of('y'); + case 'ȳ': + return Optional.of('y'); + case 'A': + return Optional.of('a'); + case 'B': + return Optional.of('b'); + case 'C': + return Optional.of('c'); + case 'D': + return Optional.of('d'); + case 'E': + return Optional.of('e'); + case 'F': + return Optional.of('f'); + case 'G': + return Optional.of('g'); + case 'H': + return Optional.of('h'); + case 'I': + return Optional.of('i'); + case 'J': + return Optional.of('j'); + case 'K': + return Optional.of('k'); + case 'L': + return Optional.of('l'); + case 'M': + return Optional.of('m'); + case 'N': + return Optional.of('n'); + case 'O': + return Optional.of('o'); + case 'P': + return Optional.of('p'); + case 'Q': + return Optional.of('q'); + case 'R': + return Optional.of('r'); + case 'S': + return Optional.of('s'); + case 'T': + return Optional.of('t'); + case 'U': + return Optional.of('u'); + case 'V': + return Optional.of('v'); + case 'W': + return Optional.of('w'); + case 'X': + return Optional.of('x'); + case 'Y': + return Optional.of('y'); + case 'Z': + return Optional.of('z'); + default: + return Optional.absent(); + } + } + + @Override + SimpleArrayMap getCharToKeyMap() { + return CHAR_TO_KEY_MAP; + } +} diff --git a/java/com/android/dialer/smartdial/map/RussianSmartDialMap.java b/java/com/android/dialer/smartdial/map/RussianSmartDialMap.java new file mode 100644 index 000000000..5038520c2 --- /dev/null +++ b/java/com/android/dialer/smartdial/map/RussianSmartDialMap.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2017 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.dialer.smartdial.map; + +import android.support.v4.util.SimpleArrayMap; +import com.google.common.base.Optional; + +/** A {@link SmartDialMap} for the Russian alphabet. */ +@SuppressWarnings("Guava") +final class RussianSmartDialMap extends SmartDialMap { + private static final SimpleArrayMap CHAR_TO_KEY_MAP = + new SimpleArrayMap<>(); + + // Reference: https://en.wikipedia.org/wiki/Russian_alphabet + static { + CHAR_TO_KEY_MAP.put('а', '2'); + CHAR_TO_KEY_MAP.put('б', '2'); + CHAR_TO_KEY_MAP.put('в', '2'); + CHAR_TO_KEY_MAP.put('г', '2'); + + CHAR_TO_KEY_MAP.put('д', '3'); + CHAR_TO_KEY_MAP.put('е', '3'); + CHAR_TO_KEY_MAP.put('ё', '3'); + CHAR_TO_KEY_MAP.put('ж', '3'); + CHAR_TO_KEY_MAP.put('з', '3'); + + CHAR_TO_KEY_MAP.put('и', '4'); + CHAR_TO_KEY_MAP.put('й', '4'); + CHAR_TO_KEY_MAP.put('к', '4'); + CHAR_TO_KEY_MAP.put('л', '4'); + + CHAR_TO_KEY_MAP.put('м', '5'); + CHAR_TO_KEY_MAP.put('н', '5'); + CHAR_TO_KEY_MAP.put('о', '5'); + CHAR_TO_KEY_MAP.put('п', '5'); + + CHAR_TO_KEY_MAP.put('р', '6'); + CHAR_TO_KEY_MAP.put('с', '6'); + CHAR_TO_KEY_MAP.put('т', '6'); + CHAR_TO_KEY_MAP.put('у', '6'); + + CHAR_TO_KEY_MAP.put('ф', '7'); + CHAR_TO_KEY_MAP.put('х', '7'); + CHAR_TO_KEY_MAP.put('ц', '7'); + CHAR_TO_KEY_MAP.put('ч', '7'); + + CHAR_TO_KEY_MAP.put('ш', '8'); + CHAR_TO_KEY_MAP.put('щ', '8'); + CHAR_TO_KEY_MAP.put('ъ', '8'); + CHAR_TO_KEY_MAP.put('ы', '8'); + + CHAR_TO_KEY_MAP.put('ь', '9'); + CHAR_TO_KEY_MAP.put('э', '9'); + CHAR_TO_KEY_MAP.put('ю', '9'); + CHAR_TO_KEY_MAP.put('я', '9'); + } + + private static RussianSmartDialMap instance; + + static RussianSmartDialMap getInstance() { + if (instance == null) { + instance = new RussianSmartDialMap(); + } + + return instance; + } + + private RussianSmartDialMap() {} + + @Override + Optional normalizeCharacter(char ch) { + ch = Character.toLowerCase(ch); + return isValidDialpadAlphabeticChar(ch) ? Optional.of(ch) : Optional.absent(); + } + + @Override + SimpleArrayMap getCharToKeyMap() { + return CHAR_TO_KEY_MAP; + } +} diff --git a/java/com/android/dialer/smartdial/map/SmartDialMap.java b/java/com/android/dialer/smartdial/map/SmartDialMap.java new file mode 100644 index 000000000..c74dd2893 --- /dev/null +++ b/java/com/android/dialer/smartdial/map/SmartDialMap.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2016 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.dialer.smartdial.map; + +import android.support.v4.util.SimpleArrayMap; +import com.google.common.base.Optional; + +/** Definition for utilities that supports smart dial in different languages. */ +@SuppressWarnings("Guava") +abstract class SmartDialMap { + + /** + * Returns true if the provided character can be mapped to a key on the dialpad. + * + *

The provided character is expected to be a normalized character. See {@link + * SmartDialMap#normalizeCharacter(char)} for details. + */ + protected boolean isValidDialpadCharacter(char ch) { + return isValidDialpadAlphabeticChar(ch) || isValidDialpadNumericChar(ch); + } + + /** + * Returns true if the provided character is a letter and can be mapped to a key on the dialpad. + * + *

The provided character is expected to be a normalized character. See {@link + * SmartDialMap#normalizeCharacter(char)} for details. + */ + protected boolean isValidDialpadAlphabeticChar(char ch) { + return getCharToKeyMap().containsKey(ch); + } + + /** + * Returns true if the provided character is a digit, and can be mapped to a key on the dialpad. + */ + protected boolean isValidDialpadNumericChar(char ch) { + return '0' <= ch && ch <= '9'; + } + + /** + * Get the index of the key on the dialpad which the character corresponds to. + * + *

The provided character is expected to be a normalized character. See {@link + * SmartDialMap#normalizeCharacter(char)} for details. + * + *

An {@link Optional#absent()} is returned if the provided character can't be mapped to a key + * on the dialpad. + */ + protected Optional getDialpadIndex(char ch) { + if (isValidDialpadNumericChar(ch)) { + return Optional.of((byte) (ch - '0')); + } + + if (isValidDialpadAlphabeticChar(ch)) { + return Optional.of((byte) (getCharToKeyMap().get(ch) - '0')); + } + + return Optional.absent(); + } + + /** + * Get the actual numeric character on the dialpad which the character corresponds to. + * + *

The provided character is expected to be a normalized character. See {@link + * SmartDialMap#normalizeCharacter(char)} for details. + * + *

An {@link Optional#absent()} is returned if the provided character can't be mapped to a key + * on the dialpad. + */ + protected Optional getDialpadNumericCharacter(char ch) { + return isValidDialpadAlphabeticChar(ch) + ? Optional.of(getCharToKeyMap().get(ch)) + : Optional.absent(); + } + + /** + * Converts uppercase characters to lower case ones, and on a best effort basis, strips accents + * from accented characters. + * + *

An {@link Optional#absent()} is returned if the provided character can't be mapped to a key + * on the dialpad. + */ + abstract Optional normalizeCharacter(char ch); + + /** + * Returns a map in which each key is a normalized character and the corresponding value is a + * dialpad key. + */ + abstract SimpleArrayMap getCharToKeyMap(); +} diff --git a/java/com/android/dialer/smartdial/map/UkrainianSmartDialMap.java b/java/com/android/dialer/smartdial/map/UkrainianSmartDialMap.java new file mode 100644 index 000000000..28dbc0d61 --- /dev/null +++ b/java/com/android/dialer/smartdial/map/UkrainianSmartDialMap.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2017 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.dialer.smartdial.map; + +import android.support.v4.util.SimpleArrayMap; +import com.google.common.base.Optional; + +/** A {@link SmartDialMap} for the Ukrainian alphabet. */ +final class UkrainianSmartDialMap extends SmartDialMap { + private static final SimpleArrayMap CHAR_TO_KEY_MAP = + new SimpleArrayMap<>(); + + // Reference: https://en.wikipedia.org/wiki/Ukrainian_alphabet + static { + CHAR_TO_KEY_MAP.put('а', '2'); + CHAR_TO_KEY_MAP.put('б', '2'); + CHAR_TO_KEY_MAP.put('в', '2'); + CHAR_TO_KEY_MAP.put('г', '2'); + CHAR_TO_KEY_MAP.put('ґ', '2'); + + CHAR_TO_KEY_MAP.put('д', '3'); + CHAR_TO_KEY_MAP.put('е', '3'); + CHAR_TO_KEY_MAP.put('є', '3'); + CHAR_TO_KEY_MAP.put('ж', '3'); + CHAR_TO_KEY_MAP.put('з', '3'); + + CHAR_TO_KEY_MAP.put('и', '4'); + CHAR_TO_KEY_MAP.put('і', '4'); + CHAR_TO_KEY_MAP.put('ї', '4'); + CHAR_TO_KEY_MAP.put('й', '4'); + CHAR_TO_KEY_MAP.put('к', '4'); + CHAR_TO_KEY_MAP.put('л', '4'); + + CHAR_TO_KEY_MAP.put('м', '5'); + CHAR_TO_KEY_MAP.put('н', '5'); + CHAR_TO_KEY_MAP.put('о', '5'); + CHAR_TO_KEY_MAP.put('п', '5'); + + CHAR_TO_KEY_MAP.put('р', '6'); + CHAR_TO_KEY_MAP.put('с', '6'); + CHAR_TO_KEY_MAP.put('т', '6'); + CHAR_TO_KEY_MAP.put('у', '6'); + + CHAR_TO_KEY_MAP.put('ф', '7'); + CHAR_TO_KEY_MAP.put('х', '7'); + CHAR_TO_KEY_MAP.put('ц', '7'); + CHAR_TO_KEY_MAP.put('ч', '7'); + + CHAR_TO_KEY_MAP.put('ш', '8'); + CHAR_TO_KEY_MAP.put('щ', '8'); + + CHAR_TO_KEY_MAP.put('ь', '9'); + CHAR_TO_KEY_MAP.put('ю', '9'); + CHAR_TO_KEY_MAP.put('я', '9'); + } + + private static UkrainianSmartDialMap instance; + + static UkrainianSmartDialMap getInstance() { + if (instance == null) { + instance = new UkrainianSmartDialMap(); + } + + return instance; + } + + private UkrainianSmartDialMap() {} + + @Override + Optional normalizeCharacter(char ch) { + ch = Character.toLowerCase(ch); + return isValidDialpadAlphabeticChar(ch) ? Optional.of(ch) : Optional.absent(); + } + + @Override + SimpleArrayMap getCharToKeyMap() { + return CHAR_TO_KEY_MAP; + } +} diff --git a/java/com/android/dialer/smartdial/util/SmartDialMatchPosition.java b/java/com/android/dialer/smartdial/util/SmartDialMatchPosition.java new file mode 100644 index 000000000..db317ae6b --- /dev/null +++ b/java/com/android/dialer/smartdial/util/SmartDialMatchPosition.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2012 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.dialer.smartdial.util; + +import com.android.dialer.common.LogUtil; +import java.util.ArrayList; + +/** + * Stores information about a range of characters matched in a display name The integers start and + * end indicate that the range start to end (exclusive) correspond to some characters in the query. + * Used to highlight certain parts of the contact's display name to indicate that those ranges + * matched the user's query. + */ +public class SmartDialMatchPosition { + + private static final String TAG = SmartDialMatchPosition.class.getSimpleName(); + + public int start; + public int end; + + public SmartDialMatchPosition(int start, int end) { + this.start = start; + this.end = end; + } + + /** + * Used by {@link SmartDialNameMatcher} to advance the positions of a match position found in a + * sub query. + * + * @param inList ArrayList of SmartDialMatchPositions to modify. + * @param toAdvance Offset to modify by. + */ + public static void advanceMatchPositions( + ArrayList inList, int toAdvance) { + for (int i = 0; i < inList.size(); i++) { + inList.get(i).advance(toAdvance); + } + } + + /** + * Used mainly for debug purposes. Displays contents of an ArrayList of SmartDialMatchPositions. + * + * @param list ArrayList of SmartDialMatchPositions to print out in a human readable fashion. + */ + public static void print(ArrayList list) { + for (int i = 0; i < list.size(); i++) { + SmartDialMatchPosition m = list.get(i); + LogUtil.d(TAG, "[" + m.start + "," + m.end + "]"); + } + } + + private void advance(int toAdvance) { + this.start += toAdvance; + this.end += toAdvance; + } +} diff --git a/java/com/android/dialer/smartdial/util/SmartDialNameMatcher.java b/java/com/android/dialer/smartdial/util/SmartDialNameMatcher.java new file mode 100644 index 000000000..725c88c57 --- /dev/null +++ b/java/com/android/dialer/smartdial/util/SmartDialNameMatcher.java @@ -0,0 +1,430 @@ +/* + * Copyright (C) 2012 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.dialer.smartdial.util; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import com.android.dialer.smartdial.map.CompositeSmartDialMap; +import com.android.dialer.smartdial.util.SmartDialPrefix.PhoneNumberTokens; +import java.util.ArrayList; + +/** + * {@link #SmartDialNameMatcher} contains utility functions to remove accents from accented + * characters and normalize a phone number. It also contains the matching logic that determines if a + * contact's display name matches a numeric query. The boolean variable {@link #ALLOW_INITIAL_MATCH} + * controls the behavior of the matching logic and determines whether we allow matches like 57 - + * (J)ohn (S)mith. + */ +public class SmartDialNameMatcher { + // Whether or not we allow matches like 57 - (J)ohn (S)mith + private static final boolean ALLOW_INITIAL_MATCH = true; + + // The maximum length of the initial we will match - typically set to 1 to minimize false + // positives + private static final int INITIAL_LENGTH_LIMIT = 1; + + private final ArrayList mMatchPositions = new ArrayList<>(); + private String mQuery; + + // Controls whether to treat an empty query as a match (with anything). + private boolean mShouldMatchEmptyQuery = false; + + public SmartDialNameMatcher(String query) { + mQuery = query; + } + + /** + * Strips a phone number of unnecessary characters (spaces, dashes, etc.) + * + * @param number Phone number we want to normalize + * @return Phone number consisting of digits from 0-9 + */ + public static String normalizeNumber(Context context, String number) { + return normalizeNumber(context, number, /* offset = */ 0); + } + + /** + * Strips a phone number of unnecessary characters (spaces, dashes, etc.) + * + * @param number Phone number we want to normalize + * @param offset Offset to start from + * @return Phone number consisting of digits from 0-9 + */ + public static String normalizeNumber(Context context, String number, int offset) { + final StringBuilder s = new StringBuilder(); + for (int i = offset; i < number.length(); i++) { + char ch = number.charAt(i); + if (CompositeSmartDialMap.isValidDialpadNumericChar(context, ch)) { + s.append(ch); + } + } + return s.toString(); + } + + /** + * Constructs empty highlight mask. Bit 0 at a position means there is no match, Bit 1 means there + * is a match and should be highlighted in the TextView. + * + * @param builder StringBuilder object + * @param length Length of the desired mask. + */ + private void constructEmptyMask(StringBuilder builder, int length) { + for (int i = 0; i < length; ++i) { + builder.append("0"); + } + } + + /** + * Replaces the 0-bit at a position with 1-bit, indicating that there is a match. + * + * @param builder StringBuilder object. + * @param matchPos Match Positions to mask as 1. + */ + private void replaceBitInMask(StringBuilder builder, SmartDialMatchPosition matchPos) { + for (int i = matchPos.start; i < matchPos.end; ++i) { + builder.replace(i, i + 1, "1"); + } + } + + /** + * Matches a phone number against a query. Let the test application overwrite the NANP setting. + * + * @param phoneNumber - Raw phone number + * @param query - Normalized query (only contains numbers from 0-9) + * @return {@literal null} if the number and the query don't match, a valid SmartDialMatchPosition + * with the matching positions otherwise + */ + @Nullable + public SmartDialMatchPosition matchesNumber(Context context, String phoneNumber, String query) { + if (TextUtils.isEmpty(phoneNumber)) { + return mShouldMatchEmptyQuery ? new SmartDialMatchPosition(0, 0) : null; + } + StringBuilder builder = new StringBuilder(); + constructEmptyMask(builder, phoneNumber.length()); + + // Try matching the number as is + SmartDialMatchPosition matchPos = + matchesNumberWithOffset(context, phoneNumber, query, /* offset = */ 0); + if (matchPos == null) { + PhoneNumberTokens phoneNumberTokens = SmartDialPrefix.parsePhoneNumber(context, phoneNumber); + + if (phoneNumberTokens.countryCodeOffset != 0) { + matchPos = + matchesNumberWithOffset( + context, phoneNumber, query, phoneNumberTokens.countryCodeOffset); + } + if (matchPos == null && phoneNumberTokens.nanpCodeOffset != 0) { + matchPos = + matchesNumberWithOffset(context, phoneNumber, query, phoneNumberTokens.nanpCodeOffset); + } + } + if (matchPos != null) { + replaceBitInMask(builder, matchPos); + } + return matchPos; + } + + /** + * Matches a phone number against the saved query, taking care of formatting characters and also + * taking into account country code prefixes and special NANP number treatment. + * + * @param phoneNumber - Raw phone number + * @return {@literal null} if the number and the query don't match, a valid SmartDialMatchPosition + * with the matching positions otherwise + */ + public SmartDialMatchPosition matchesNumber(Context context, String phoneNumber) { + return matchesNumber(context, phoneNumber, mQuery); + } + + /** + * Matches a phone number against a query, taking care of formatting characters + * + * @param phoneNumber - Raw phone number + * @param query - Normalized query (only contains numbers from 0-9) + * @param offset - The position in the number to start the match against (used to ignore leading + * prefixes/country codes) + * @return {@literal null} if the number and the query don't match, a valid SmartDialMatchPosition + * with the matching positions otherwise + */ + private SmartDialMatchPosition matchesNumberWithOffset( + Context context, String phoneNumber, String query, int offset) { + if (TextUtils.isEmpty(phoneNumber) || TextUtils.isEmpty(query)) { + return mShouldMatchEmptyQuery ? new SmartDialMatchPosition(offset, offset) : null; + } + int queryAt = 0; + int numberAt = offset; + for (int i = offset; i < phoneNumber.length(); i++) { + if (queryAt == query.length()) { + break; + } + char ch = phoneNumber.charAt(i); + if (CompositeSmartDialMap.isValidDialpadNumericChar(context, ch)) { + if (ch != query.charAt(queryAt)) { + return null; + } + queryAt++; + } else { + if (queryAt == 0) { + // Found a separator before any part of the query was matched, so advance the + // offset to avoid prematurely highlighting separators before the rest of the + // query. + // E.g. don't highlight the first '-' if we're matching 1-510-111-1111 with + // '510'. + // However, if the current offset is 0, just include the beginning separators + // anyway, otherwise the highlighting ends up looking weird. + // E.g. if we're matching (510)-111-1111 with '510', we should include the + // first '('. + if (offset != 0) { + offset++; + } + } + } + numberAt++; + } + return new SmartDialMatchPosition(0 + offset, numberAt); + } + + /** + * This function iterates through each token in the display name, trying to match the query to the + * numeric equivalent of the token. + * + *

A token is defined as a range in the display name delimited by characters that have no latin + * alphabet equivalents (e.g. spaces - ' ', periods - ',', underscores - '_' or chinese characters + * - '王'). Transliteration from non-latin characters to latin character will be done on a best + * effort basis - e.g. 'Ü' - 'u'. + * + *

For example, the display name "Phillips Thomas Jr" contains three tokens: "phillips", + * "thomas", and "jr". + * + *

A match must begin at the start of a token. For example, typing 846(Tho) would match + * "Phillips Thomas", but 466(hom) would not. + * + *

Also, a match can extend across tokens. For example, typing 37337(FredS) would match (Fred + * S)mith. + * + * @param displayName The normalized(no accented characters) display name we intend to match + * against. + * @param query The string of digits that we want to match the display name to. + * @param matchList An array list of {@link SmartDialMatchPosition}s that we add matched positions + * to. + * @return Returns true if a combination of the tokens in displayName match the query string + * contained in query. If the function returns true, matchList will contain an ArrayList of + * match positions (multiple matches correspond to initial matches). + */ + private boolean matchesCombination( + Context context, + String displayName, + String query, + ArrayList matchList) { + StringBuilder builder = new StringBuilder(); + constructEmptyMask(builder, displayName.length()); + final int nameLength = displayName.length(); + final int queryLength = query.length(); + + if (nameLength < queryLength) { + return false; + } + + if (queryLength == 0) { + return false; + } + + // The current character index in displayName + // E.g. 3 corresponds to 'd' in "Fred Smith" + int nameStart = 0; + + // The current character in the query we are trying to match the displayName against + int queryStart = 0; + + // The start position of the current token we are inspecting + int tokenStart = 0; + + // The number of non-alphabetic characters we've encountered so far in the current match. + // E.g. if we've currently matched 3733764849 to (Fred Smith W)illiam, then the + // seperatorCount should be 2. This allows us to correctly calculate offsets for the match + // positions + int seperatorCount = 0; + + ArrayList partial = new ArrayList(); + // Keep going until we reach the end of displayName + while (nameStart < nameLength && queryStart < queryLength) { + char ch = displayName.charAt(nameStart); + // Strip diacritics from accented characters if any + ch = CompositeSmartDialMap.normalizeCharacter(context, ch); + if (CompositeSmartDialMap.isValidDialpadCharacter(context, ch)) { + if (CompositeSmartDialMap.isValidDialpadAlphabeticChar(context, ch)) { + ch = CompositeSmartDialMap.getDialpadNumericCharacter(context, ch); + } + if (ch != query.charAt(queryStart)) { + // Failed to match the current character in the query. + + // Case 1: Failed to match the first character in the query. Skip to the next + // token since there is no chance of this token matching the query. + + // Case 2: Previous characters in the query matched, but the current character + // failed to match. This happened in the middle of a token. Skip to the next + // token since there is no chance of this token matching the query. + + // Case 3: Previous characters in the query matched, but the current character + // failed to match. This happened right at the start of the current token. In + // this case, we should restart the query and try again with the current token. + // Otherwise, we would fail to match a query like "964"(yog) against a name + // Yo-Yoghurt because the query match would fail on the 3rd character, and + // then skip to the end of the "Yoghurt" token. + + if (queryStart == 0 + || CompositeSmartDialMap.isValidDialpadCharacter( + context, + CompositeSmartDialMap.normalizeCharacter( + context, displayName.charAt(nameStart - 1)))) { + // skip to the next token, in the case of 1 or 2. + while (nameStart < nameLength + && CompositeSmartDialMap.isValidDialpadCharacter( + context, + CompositeSmartDialMap.normalizeCharacter( + context, displayName.charAt(nameStart)))) { + nameStart++; + } + nameStart++; + } + + // Restart the query and set the correct token position + queryStart = 0; + seperatorCount = 0; + tokenStart = nameStart; + } else { + if (queryStart == queryLength - 1) { + + // As much as possible, we prioritize a full token match over a sub token + // one so if we find a full token match, we can return right away + matchList.add( + new SmartDialMatchPosition(tokenStart, queryLength + tokenStart + seperatorCount)); + for (SmartDialMatchPosition match : matchList) { + replaceBitInMask(builder, match); + } + return true; + } else if (ALLOW_INITIAL_MATCH && queryStart < INITIAL_LENGTH_LIMIT) { + // we matched the first character. + // branch off and see if we can find another match with the remaining + // characters in the query string and the remaining tokens + // find the next separator in the query string + int j; + for (j = nameStart; j < nameLength; j++) { + if (!CompositeSmartDialMap.isValidDialpadCharacter( + context, + CompositeSmartDialMap.normalizeCharacter(context, displayName.charAt(j)))) { + break; + } + } + // this means there is at least one character left after the separator + if (j < nameLength - 1) { + final String remainder = displayName.substring(j + 1); + final ArrayList partialTemp = new ArrayList<>(); + if (matchesCombination( + context, remainder, query.substring(queryStart + 1), partialTemp)) { + + // store the list of possible match positions + SmartDialMatchPosition.advanceMatchPositions(partialTemp, j + 1); + partialTemp.add(0, new SmartDialMatchPosition(nameStart, nameStart + 1)); + // we found a partial token match, store the data in a + // temp buffer and return it if we end up not finding a full + // token match + partial = partialTemp; + } + } + } + nameStart++; + queryStart++; + // we matched the current character in the name against one in the query, + // continue and see if the rest of the characters match + } + } else { + // found a separator, we skip this character and continue to the next one + nameStart++; + if (queryStart == 0) { + // This means we found a separator before the start of a token, + // so we should increment the token's start position to reflect its true + // start position + tokenStart = nameStart; + } else { + // Otherwise this separator was found in the middle of a token being matched, + // so increase the separator count + seperatorCount++; + } + } + } + // if we have no complete match at this point, then we attempt to fall back to the partial + // token match(if any). If we don't allow initial matching (ALLOW_INITIAL_MATCH = false) + // then partial will always be empty. + if (!partial.isEmpty()) { + matchList.addAll(partial); + for (SmartDialMatchPosition match : matchList) { + replaceBitInMask(builder, match); + } + return true; + } + return false; + } + + /** + * This function iterates through each token in the display name, trying to match the query to the + * numeric equivalent of the token. + * + *

A token is defined as a range in the display name delimited by characters that have no latin + * alphabet equivalents (e.g. spaces - ' ', periods - ',', underscores - '_' or chinese characters + * - '王'). Transliteration from non-latin characters to latin character will be done on a best + * effort basis - e.g. 'Ü' - 'u'. + * + *

For example, the display name "Phillips Thomas Jr" contains three tokens: "phillips", + * "thomas", and "jr". + * + *

A match must begin at the start of a token. For example, typing 846(Tho) would match + * "Phillips Thomas", but 466(hom) would not. + * + *

Also, a match can extend across tokens. For example, typing 37337(FredS) would match (Fred + * S)mith. + * + * @param displayName The normalized(no accented characters) display name we intend to match + * against. + * @return Returns true if a combination of the tokens in displayName match the query string + * contained in query. If the function returns true, matchList will contain an ArrayList of + * match positions (multiple matches correspond to initial matches). + */ + public boolean matches(Context context, String displayName) { + mMatchPositions.clear(); + return matchesCombination(context, displayName, mQuery, mMatchPositions); + } + + public ArrayList getMatchPositions() { + // Return a clone of mMatchPositions so that the caller can use it without + // worrying about it changing + return new ArrayList<>(mMatchPositions); + } + + public String getQuery() { + return mQuery; + } + + public void setQuery(String query) { + mQuery = query; + } + + public void setShouldMatchEmptyQuery(boolean matches) { + mShouldMatchEmptyQuery = matches; + } +} diff --git a/java/com/android/dialer/smartdial/util/SmartDialPrefix.java b/java/com/android/dialer/smartdial/util/SmartDialPrefix.java new file mode 100644 index 000000000..9af411913 --- /dev/null +++ b/java/com/android/dialer/smartdial/util/SmartDialPrefix.java @@ -0,0 +1,602 @@ +/* + * Copyright (C) 2013 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.dialer.smartdial.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.support.annotation.VisibleForTesting; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import com.android.dialer.smartdial.map.CompositeSmartDialMap; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +/** + * Smart Dial utility class to find prefixes of contacts. It contains both methods to find supported + * prefix combinations for contact names, and also methods to find supported prefix combinations for + * contacts' phone numbers. Each contact name is separated into several tokens, such as first name, + * middle name, family name etc. Each phone number is also separated into country code, NANP area + * code, and local number if such separation is possible. + */ +public class SmartDialPrefix { + + /** + * The number of starting and ending tokens in a contact's name considered for initials. For + * example, if both constants are set to 2, and a contact's name is "Albert Ben Charles Daniel Ed + * Foster", the first two tokens "Albert" "Ben", and last two tokens "Ed" "Foster" can be replaced + * by their initials in contact name matching. Users can look up this contact by combinations of + * his initials such as "AF" "BF" "EF" "ABF" "BEF" "ABEF" etc, but can not use combinations such + * as "CF" "DF" "ACF" "ADF" etc. + */ + private static final int LAST_TOKENS_FOR_INITIALS = 2; + + private static final int FIRST_TOKENS_FOR_INITIALS = 2; + + /** The country code of the user's sim card obtained by calling getSimCountryIso */ + private static final String PREF_USER_SIM_COUNTRY_CODE = + "DialtactsActivity_user_sim_country_code"; + + private static final String PREF_USER_SIM_COUNTRY_CODE_DEFAULT = null; + + private static String sUserSimCountryCode = PREF_USER_SIM_COUNTRY_CODE_DEFAULT; + /** Indicates whether user is in NANP regions. */ + private static boolean sUserInNanpRegion = false; + /** Set of country names that use NANP code. */ + private static Set sNanpCountries = null; + /** Set of supported country codes in front of the phone number. */ + private static Set sCountryCodes = null; + + private static boolean sNanpInitialized = false; + + /** Initializes the Nanp settings, and finds out whether user is in a NANP region. */ + public static void initializeNanpSettings(Context context) { + final TelephonyManager manager = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + if (manager != null) { + sUserSimCountryCode = manager.getSimCountryIso(); + } + + final SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); + + if (sUserSimCountryCode != null) { + /** Updates shared preferences with the latest country obtained from getSimCountryIso. */ + prefs.edit().putString(PREF_USER_SIM_COUNTRY_CODE, sUserSimCountryCode).apply(); + } else { + /** Uses previously stored country code if loading fails. */ + sUserSimCountryCode = + prefs.getString(PREF_USER_SIM_COUNTRY_CODE, PREF_USER_SIM_COUNTRY_CODE_DEFAULT); + } + /** Queries the NANP country list to find out whether user is in a NANP region. */ + sUserInNanpRegion = isCountryNanp(sUserSimCountryCode); + sNanpInitialized = true; + } + + /** + * Parses a contact's name into a list of separated tokens. + * + * @param contactName Contact's name stored in string. + * @return A list of name tokens, for example separated first names, last name, etc. + */ + public static ArrayList parseToIndexTokens(Context context, String contactName) { + final int length = contactName.length(); + final ArrayList result = new ArrayList<>(); + char c; + final StringBuilder currentIndexToken = new StringBuilder(); + /** + * Iterates through the whole name string. If the current character is a valid character, append + * it to the current token. If the current character is not a valid character, for example space + * " ", mark the current token as complete and add it to the list of tokens. + */ + for (int i = 0; i < length; i++) { + c = CompositeSmartDialMap.normalizeCharacter(context, contactName.charAt(i)); + if (CompositeSmartDialMap.isValidDialpadCharacter(context, c)) { + /** Converts a character into the number on dialpad that represents the character. */ + currentIndexToken.append(CompositeSmartDialMap.getDialpadIndex(context, c)); + } else { + if (currentIndexToken.length() != 0) { + result.add(currentIndexToken.toString()); + } + currentIndexToken.delete(0, currentIndexToken.length()); + } + } + + /** Adds the last token in case it has not been added. */ + if (currentIndexToken.length() != 0) { + result.add(currentIndexToken.toString()); + } + return result; + } + + /** + * Generates a list of strings that any prefix of any string in the list can be used to look up + * the contact's name. + * + * @param index The contact's name in string. + * @return A List of strings, whose prefix can be used to look up the contact. + */ + public static ArrayList generateNamePrefixes(Context context, String index) { + final ArrayList result = new ArrayList<>(); + + /** Parses the name into a list of tokens. */ + final ArrayList indexTokens = parseToIndexTokens(context, index); + + if (indexTokens.size() > 0) { + /** + * Adds the full token combinations to the list. For example, a contact with name "Albert Ben + * Ed Foster" can be looked up by any prefix of the following strings "Foster" "EdFoster" + * "BenEdFoster" and "AlbertBenEdFoster". This covers all cases of look up that contains only + * one token, and that spans multiple continuous tokens. + */ + final StringBuilder fullNameToken = new StringBuilder(); + for (int i = indexTokens.size() - 1; i >= 0; i--) { + fullNameToken.insert(0, indexTokens.get(i)); + result.add(fullNameToken.toString()); + } + + /** + * Adds initial combinations to the list, with the number of initials restricted by {@link + * #LAST_TOKENS_FOR_INITIALS} and {@link #FIRST_TOKENS_FOR_INITIALS}. For example, a contact + * with name "Albert Ben Ed Foster" can be looked up by any prefix of the following strings + * "EFoster" "BFoster" "BEFoster" "AFoster" "ABFoster" "AEFoster" and "ABEFoster". This covers + * all cases of initial lookup. + */ + ArrayList fullNames = new ArrayList<>(); + fullNames.add(indexTokens.get(indexTokens.size() - 1)); + final int recursiveNameStart = result.size(); + int recursiveNameEnd = result.size(); + String initial = ""; + for (int i = indexTokens.size() - 2; i >= 0; i--) { + if ((i >= indexTokens.size() - LAST_TOKENS_FOR_INITIALS) + || (i < FIRST_TOKENS_FOR_INITIALS)) { + initial = indexTokens.get(i).substring(0, 1); + + /** Recursively adds initial combinations to the list. */ + for (int j = 0; j < fullNames.size(); ++j) { + result.add(initial + fullNames.get(j)); + } + for (int j = recursiveNameStart; j < recursiveNameEnd; ++j) { + result.add(initial + result.get(j)); + } + recursiveNameEnd = result.size(); + final String currentFullName = fullNames.get(fullNames.size() - 1); + fullNames.add(indexTokens.get(i) + currentFullName); + } + } + } + + return result; + } + + /** + * Computes a list of number strings based on tokens of a given phone number. Any prefix of any + * string in the list can be used to look up the phone number. The list include the full phone + * number, the national number if there is a country code in the phone number, and the local + * number if there is an area code in the phone number following the NANP format. For example, if + * a user has phone number +41 71 394 8392, the list will contain 41713948392 and 713948392. Any + * prefix to either of the strings can be used to look up the phone number. If a user has a phone + * number +1 555-302-3029 (NANP format), the list will contain 15553023029, 5553023029, and + * 3023029. + * + * @param number String of user's phone number. + * @return A list of strings where any prefix of any entry can be used to look up the number. + */ + public static ArrayList parseToNumberTokens(Context context, String number) { + final ArrayList result = new ArrayList<>(); + if (!TextUtils.isEmpty(number)) { + /** Adds the full number to the list. */ + result.add(SmartDialNameMatcher.normalizeNumber(context, number)); + + final PhoneNumberTokens phoneNumberTokens = parsePhoneNumber(context, number); + if (phoneNumberTokens == null) { + return result; + } + + if (phoneNumberTokens.countryCodeOffset != 0) { + result.add( + SmartDialNameMatcher.normalizeNumber( + context, number, phoneNumberTokens.countryCodeOffset)); + } + + if (phoneNumberTokens.nanpCodeOffset != 0) { + result.add( + SmartDialNameMatcher.normalizeNumber( + context, number, phoneNumberTokens.nanpCodeOffset)); + } + } + return result; + } + + /** + * Parses a phone number to find out whether it has country code and NANP area code. + * + * @param number Raw phone number. + * @return a PhoneNumberToken instance with country code, NANP code information. + */ + public static PhoneNumberTokens parsePhoneNumber(Context context, String number) { + String countryCode = ""; + int countryCodeOffset = 0; + int nanpNumberOffset = 0; + + if (!TextUtils.isEmpty(number)) { + String normalizedNumber = SmartDialNameMatcher.normalizeNumber(context, number); + if (number.charAt(0) == '+') { + /** If the number starts with '+', tries to find valid country code. */ + for (int i = 1; i <= 1 + 3; i++) { + if (number.length() <= i) { + break; + } + countryCode = number.substring(1, i); + if (isValidCountryCode(countryCode)) { + countryCodeOffset = i; + break; + } + } + } else { + /** + * If the number does not start with '+', finds out whether it is in NANP format and has '1' + * preceding the number. + */ + if ((normalizedNumber.length() == 11) + && (normalizedNumber.charAt(0) == '1') + && (sUserInNanpRegion)) { + countryCode = "1"; + countryCodeOffset = number.indexOf(normalizedNumber.charAt(1)); + if (countryCodeOffset == -1) { + countryCodeOffset = 0; + } + } + } + + /** If user is in NANP region, finds out whether a number is in NANP format. */ + if (sUserInNanpRegion) { + String areaCode = ""; + if (countryCode.equals("") && normalizedNumber.length() == 10) { + /** + * if the number has no country code but fits the NANP format, extracts the NANP area + * code, and finds out offset of the local number. + */ + areaCode = normalizedNumber.substring(0, 3); + } else if (countryCode.equals("1") && normalizedNumber.length() == 11) { + /** + * If the number has country code '1', finds out area code and offset of the local number. + */ + areaCode = normalizedNumber.substring(1, 4); + } + if (!areaCode.equals("")) { + final int areaCodeIndex = number.indexOf(areaCode); + if (areaCodeIndex != -1) { + nanpNumberOffset = number.indexOf(areaCode) + 3; + } + } + } + } + return new PhoneNumberTokens(countryCode, countryCodeOffset, nanpNumberOffset); + } + + /** Checkes whether a country code is valid. */ + private static boolean isValidCountryCode(String countryCode) { + if (sCountryCodes == null) { + sCountryCodes = initCountryCodes(); + } + return sCountryCodes.contains(countryCode); + } + + private static Set initCountryCodes() { + final HashSet result = new HashSet(); + result.add("1"); + result.add("7"); + result.add("20"); + result.add("27"); + result.add("30"); + result.add("31"); + result.add("32"); + result.add("33"); + result.add("34"); + result.add("36"); + result.add("39"); + result.add("40"); + result.add("41"); + result.add("43"); + result.add("44"); + result.add("45"); + result.add("46"); + result.add("47"); + result.add("48"); + result.add("49"); + result.add("51"); + result.add("52"); + result.add("53"); + result.add("54"); + result.add("55"); + result.add("56"); + result.add("57"); + result.add("58"); + result.add("60"); + result.add("61"); + result.add("62"); + result.add("63"); + result.add("64"); + result.add("65"); + result.add("66"); + result.add("81"); + result.add("82"); + result.add("84"); + result.add("86"); + result.add("90"); + result.add("91"); + result.add("92"); + result.add("93"); + result.add("94"); + result.add("95"); + result.add("98"); + result.add("211"); + result.add("212"); + result.add("213"); + result.add("216"); + result.add("218"); + result.add("220"); + result.add("221"); + result.add("222"); + result.add("223"); + result.add("224"); + result.add("225"); + result.add("226"); + result.add("227"); + result.add("228"); + result.add("229"); + result.add("230"); + result.add("231"); + result.add("232"); + result.add("233"); + result.add("234"); + result.add("235"); + result.add("236"); + result.add("237"); + result.add("238"); + result.add("239"); + result.add("240"); + result.add("241"); + result.add("242"); + result.add("243"); + result.add("244"); + result.add("245"); + result.add("246"); + result.add("247"); + result.add("248"); + result.add("249"); + result.add("250"); + result.add("251"); + result.add("252"); + result.add("253"); + result.add("254"); + result.add("255"); + result.add("256"); + result.add("257"); + result.add("258"); + result.add("260"); + result.add("261"); + result.add("262"); + result.add("263"); + result.add("264"); + result.add("265"); + result.add("266"); + result.add("267"); + result.add("268"); + result.add("269"); + result.add("290"); + result.add("291"); + result.add("297"); + result.add("298"); + result.add("299"); + result.add("350"); + result.add("351"); + result.add("352"); + result.add("353"); + result.add("354"); + result.add("355"); + result.add("356"); + result.add("357"); + result.add("358"); + result.add("359"); + result.add("370"); + result.add("371"); + result.add("372"); + result.add("373"); + result.add("374"); + result.add("375"); + result.add("376"); + result.add("377"); + result.add("378"); + result.add("379"); + result.add("380"); + result.add("381"); + result.add("382"); + result.add("385"); + result.add("386"); + result.add("387"); + result.add("389"); + result.add("420"); + result.add("421"); + result.add("423"); + result.add("500"); + result.add("501"); + result.add("502"); + result.add("503"); + result.add("504"); + result.add("505"); + result.add("506"); + result.add("507"); + result.add("508"); + result.add("509"); + result.add("590"); + result.add("591"); + result.add("592"); + result.add("593"); + result.add("594"); + result.add("595"); + result.add("596"); + result.add("597"); + result.add("598"); + result.add("599"); + result.add("670"); + result.add("672"); + result.add("673"); + result.add("674"); + result.add("675"); + result.add("676"); + result.add("677"); + result.add("678"); + result.add("679"); + result.add("680"); + result.add("681"); + result.add("682"); + result.add("683"); + result.add("685"); + result.add("686"); + result.add("687"); + result.add("688"); + result.add("689"); + result.add("690"); + result.add("691"); + result.add("692"); + result.add("800"); + result.add("808"); + result.add("850"); + result.add("852"); + result.add("853"); + result.add("855"); + result.add("856"); + result.add("870"); + result.add("878"); + result.add("880"); + result.add("881"); + result.add("882"); + result.add("883"); + result.add("886"); + result.add("888"); + result.add("960"); + result.add("961"); + result.add("962"); + result.add("963"); + result.add("964"); + result.add("965"); + result.add("966"); + result.add("967"); + result.add("968"); + result.add("970"); + result.add("971"); + result.add("972"); + result.add("973"); + result.add("974"); + result.add("975"); + result.add("976"); + result.add("977"); + result.add("979"); + result.add("992"); + result.add("993"); + result.add("994"); + result.add("995"); + result.add("996"); + result.add("998"); + return result; + } + + /** + * Indicates whether the given country uses NANP numbers + * + * @param country ISO 3166 country code (case doesn't matter) + * @return True if country uses NANP numbers (e.g. US, Canada), false otherwise + * @see + * https://en.wikipedia.org/wiki/North_American_Numbering_Plan + */ + @VisibleForTesting + public static boolean isCountryNanp(String country) { + if (TextUtils.isEmpty(country)) { + return false; + } + if (sNanpCountries == null) { + sNanpCountries = initNanpCountries(); + } + return sNanpCountries.contains(country.toUpperCase()); + } + + private static Set initNanpCountries() { + final HashSet result = new HashSet(); + result.add("US"); // United States + result.add("CA"); // Canada + result.add("AS"); // American Samoa + result.add("AI"); // Anguilla + result.add("AG"); // Antigua and Barbuda + result.add("BS"); // Bahamas + result.add("BB"); // Barbados + result.add("BM"); // Bermuda + result.add("VG"); // British Virgin Islands + result.add("KY"); // Cayman Islands + result.add("DM"); // Dominica + result.add("DO"); // Dominican Republic + result.add("GD"); // Grenada + result.add("GU"); // Guam + result.add("JM"); // Jamaica + result.add("PR"); // Puerto Rico + result.add("MS"); // Montserrat + result.add("MP"); // Northern Mariana Islands + result.add("KN"); // Saint Kitts and Nevis + result.add("LC"); // Saint Lucia + result.add("VC"); // Saint Vincent and the Grenadines + result.add("TT"); // Trinidad and Tobago + result.add("TC"); // Turks and Caicos Islands + result.add("VI"); // U.S. Virgin Islands + return result; + } + + /** + * Returns whether the user is in a region that uses Nanp format based on the sim location. + * + * @return Whether user is in Nanp region. + */ + public static boolean getUserInNanpRegion() { + return sUserInNanpRegion; + } + + /** Explicitly setting the user Nanp to the given boolean */ + @VisibleForTesting + public static void setUserInNanpRegion(boolean userInNanpRegion) { + sUserInNanpRegion = userInNanpRegion; + } + + /** Class to record phone number parsing information. */ + public static class PhoneNumberTokens { + + /** Country code of the phone number. */ + final String countryCode; + + /** Offset of national number after the country code. */ + final int countryCodeOffset; + + /** Offset of local number after NANP area code. */ + final int nanpCodeOffset; + + public PhoneNumberTokens(String countryCode, int countryCodeOffset, int nanpCodeOffset) { + this.countryCode = countryCode; + this.countryCodeOffset = countryCodeOffset; + this.nanpCodeOffset = nanpCodeOffset; + } + } +} -- cgit v1.2.3