summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml8
-rw-r--r--res/layout/dialpad_fragment.xml18
-rw-r--r--res/layout/dialpad_smartdial_item.xml28
-rw-r--r--res/mipmap-hdpi/ic_launcher_phone.pngbin0 -> 7524 bytes
-rw-r--r--res/mipmap-mdpi/ic_launcher_phone.pngbin0 -> 4350 bytes
-rw-r--r--res/mipmap-xhdpi/ic_launcher_phone.pngbin0 -> 11180 bytes
-rw-r--r--res/mipmap-xxhdpi/ic_launcher_phone.pngbin0 -> 17308 bytes
-rw-r--r--res/values/colors.xml2
-rw-r--r--res/values/dimens.xml5
-rw-r--r--res/values/styles.xml6
-rw-r--r--src/com/android/dialer/DialtactsActivity.java2
-rw-r--r--src/com/android/dialer/calllog/CallLogFragment.java49
-rw-r--r--src/com/android/dialer/dialpad/DialpadFragment.java181
-rw-r--r--src/com/android/dialer/dialpad/SmartDialAdapter.java169
-rw-r--r--src/com/android/dialer/dialpad/SmartDialEntry.java36
-rw-r--r--src/com/android/dialer/dialpad/SmartDialLoaderTask.java254
-rw-r--r--src/com/android/dialer/dialpad/SmartDialMatchPosition.java70
-rw-r--r--src/com/android/dialer/dialpad/SmartDialNameMatcher.java552
-rw-r--r--src/com/android/dialer/dialpad/SmartDialTextView.java103
-rw-r--r--tests/src/com/android/dialer/SmartDialNameMatcherTest.java123
20 files changed, 1542 insertions, 64 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 7c39ee5db..502184c63 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -51,10 +51,8 @@
<application
android:label="@string/applicationLabel"
- android:icon="@mipmap/ic_launcher_contacts"
- android:taskAffinity="android.task.contacts"
- android:hardwareAccelerated="true"
- >
+ android:icon="@mipmap/ic_launcher_phone"
+ android:hardwareAccelerated="true">
<!-- The entrance point for Phone UI.
stateAlwaysHidden is set to suppress keyboard show up on
@@ -68,7 +66,6 @@
android:icon="@mipmap/ic_launcher_phone"
android:screenOrientation="nosensor"
android:enabled="@*android:bool/config_voice_capable"
- android:taskAffinity="android.task.contacts.phone"
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing">
<intent-filter>
<action android:name="android.intent.action.DIAL" />
@@ -130,7 +127,6 @@
android:theme="@style/CallDetailActivityTheme"
android:screenOrientation="portrait"
android:icon="@mipmap/ic_launcher_phone"
- android:taskAffinity="android.task.contacts.phone"
>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
diff --git a/res/layout/dialpad_fragment.xml b/res/layout/dialpad_fragment.xml
index 6423638a3..e672551bb 100644
--- a/res/layout/dialpad_fragment.xml
+++ b/res/layout/dialpad_fragment.xml
@@ -56,13 +56,23 @@
android:src="@drawable/ic_dial_action_delete" />
</LinearLayout>
+ <View style="@style/DialpadHorizontalSeparator"/>
+
+ <!-- Smard dial suggestion section -->
+ <GridView
+ android:id="@+id/dialpad_smartdial_list"
+ android:layout_width="match_parent"
+ android:layout_height="42sp"
+ android:columnWidth="0dp"
+ android:numColumns="3"
+ android:stretchMode="columnWidth"
+ android:gravity="center"
+ android:background="@drawable/dialpad_background"/>
+
<!-- Keypad section -->
<include layout="@layout/dialpad" />
- <View
- android:layout_width="match_parent"
- android:layout_height="@dimen/dialpad_vertical_margin"
- android:background="#66000000"/>
+ <View style="@style/DialpadHorizontalSeparator"/>
<!-- left and right paddings will be modified by the code. See DialpadFragment. -->
<FrameLayout
diff --git a/res/layout/dialpad_smartdial_item.xml b/res/layout/dialpad_smartdial_item.xml
new file mode 100644
index 000000000..eed257035
--- /dev/null
+++ b/res/layout/dialpad_smartdial_item.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<com.android.dialer.dialpad.SmartDialTextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/contact_name"
+ android:layout_width="match_parent"
+ android:layout_height="42sp"
+ android:padding="@dimen/smartdial_suggestions_padding"
+ android:textColor="#39caff"
+ android:textSize="16sp"
+ android:singleLine="true"
+ android:ellipsize="none"
+ android:gravity="center"
+ />
diff --git a/res/mipmap-hdpi/ic_launcher_phone.png b/res/mipmap-hdpi/ic_launcher_phone.png
new file mode 100644
index 000000000..5a3dff1f3
--- /dev/null
+++ b/res/mipmap-hdpi/ic_launcher_phone.png
Binary files differ
diff --git a/res/mipmap-mdpi/ic_launcher_phone.png b/res/mipmap-mdpi/ic_launcher_phone.png
new file mode 100644
index 000000000..9ea0d8c8b
--- /dev/null
+++ b/res/mipmap-mdpi/ic_launcher_phone.png
Binary files differ
diff --git a/res/mipmap-xhdpi/ic_launcher_phone.png b/res/mipmap-xhdpi/ic_launcher_phone.png
new file mode 100644
index 000000000..e97836cdf
--- /dev/null
+++ b/res/mipmap-xhdpi/ic_launcher_phone.png
Binary files differ
diff --git a/res/mipmap-xxhdpi/ic_launcher_phone.png b/res/mipmap-xxhdpi/ic_launcher_phone.png
new file mode 100644
index 000000000..1594e4ec3
--- /dev/null
+++ b/res/mipmap-xxhdpi/ic_launcher_phone.png
Binary files differ
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 9f3e3a2b0..2828725ac 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -18,5 +18,7 @@
<!-- Secondary text color in the Phone app -->
<color name="dialtacts_secondary_text_color">#888888</color>
+ <color name="smartdial_confidence_drawable_color">#39caff</color>
+ <color name="smartdial_highlighted_text_color">#ffffff</color>
</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 2c8d59662..4034f7303 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -50,4 +50,9 @@
<!-- Min with of fake menu buttons, which should be same as ActionBar's one -->
<dimen name="fake_menu_button_min_width">56dip</dimen>
+
+ <!-- Smart Dial -->
+ <dimen name="smartdial_suggestions_padding">4dp</dimen>
+ <dimen name="smartdial_suggestions_extra_padding">2dp</dimen>
+ <dimen name="smartdial_confidence_hint_text_size">27dp</dimen>
</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 6f36bf1d8..4f64cb37a 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -104,6 +104,12 @@
<item name="android:soundEffectsEnabled">false</item>
</style>
+ <style name="DialpadHorizontalSeparator">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">@dimen/dialpad_vertical_margin</item>
+ <item name="android:background">#66000000</item>
+ </style>
+
<style name="DialtactsActionBarStyle" parent="android:Widget.Holo.ActionBar">
<item name="android:backgroundSplit">@null</item>
<item name="android:backgroundStacked">@drawable/ab_stacked_opaque_dark_holo</item>
diff --git a/src/com/android/dialer/DialtactsActivity.java b/src/com/android/dialer/DialtactsActivity.java
index 12b29a8ad..0edf19a3b 100644
--- a/src/com/android/dialer/DialtactsActivity.java
+++ b/src/com/android/dialer/DialtactsActivity.java
@@ -828,7 +828,7 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O
if (mViewPager.getCurrentItem() == TAB_INDEX_DIALER) {
if (mDialpadFragment != null) {
- mDialpadFragment.configureScreenFromIntent(newIntent);
+ mDialpadFragment.setStartedFromNewIntent(true);
} else {
Log.e(TAG, "DialpadFragment isn't ready yet when the tab is already selected.");
}
diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java
index eb0b371b2..86a383aa9 100644
--- a/src/com/android/dialer/calllog/CallLogFragment.java
+++ b/src/com/android/dialer/calllog/CallLogFragment.java
@@ -77,8 +77,6 @@ public class CallLogFragment extends ListFragment
/** Whether there is at least one voicemail source installed. */
private boolean mVoicemailSourcesAvailable = false;
- /** Whether we are currently filtering over voicemail. */
- private boolean mShowingVoicemailOnly = false;
private VoicemailStatusHelper mVoicemailStatusHelper;
private View mStatusMessageView;
@@ -317,10 +315,6 @@ public class CallLogFragment extends ListFragment
public void startCallsQuery() {
mAdapter.setLoading(true);
mCallLogQueryHandler.fetchCalls(mCallTypeFilter);
- if (mShowingVoicemailOnly) {
- mShowingVoicemailOnly = false;
- getActivity().invalidateOptionsMenu();
- }
}
private void startVoicemailStatusQuery() {
@@ -340,10 +334,49 @@ public class CallLogFragment extends ListFragment
// menu items are ready if the first item is non-null.
if (itemDeleteAll != null) {
itemDeleteAll.setEnabled(mAdapter != null && !mAdapter.isEmpty());
- menu.findItem(R.id.show_voicemails_only).setVisible(mVoicemailSourcesAvailable);
+
+ showAllFilterMenuOptions(menu);
+ hideCurrentFilterMenuOption(menu);
+
+ // Only hide if not available. Let the above calls handle showing.
+ if (!mVoicemailSourcesAvailable) {
+ menu.findItem(R.id.show_voicemails_only).setVisible(false);
+ }
+ }
+ }
+
+ private void hideCurrentFilterMenuOption(Menu menu) {
+ MenuItem item = null;
+ switch (mCallTypeFilter) {
+ case CallLogQueryHandler.CALL_TYPE_ALL:
+ item = menu.findItem(R.id.show_all_calls);
+ break;
+ case Calls.INCOMING_TYPE:
+ item = menu.findItem(R.id.show_incoming_only);
+ break;
+ case Calls.OUTGOING_TYPE:
+ item = menu.findItem(R.id.show_outgoing_only);
+ break;
+ case Calls.MISSED_TYPE:
+ item = menu.findItem(R.id.show_missed_only);
+ break;
+ case Calls.VOICEMAIL_TYPE:
+ menu.findItem(R.id.show_voicemails_only);
+ break;
+ }
+ if (item != null) {
+ item.setVisible(false);
}
}
+ private void showAllFilterMenuOptions(Menu menu) {
+ menu.findItem(R.id.show_all_calls).setVisible(true);
+ menu.findItem(R.id.show_incoming_only).setVisible(true);
+ menu.findItem(R.id.show_outgoing_only).setVisible(true);
+ menu.findItem(R.id.show_missed_only).setVisible(true);
+ menu.findItem(R.id.show_voicemails_only).setVisible(true);
+ }
+
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
@@ -375,7 +408,6 @@ public class CallLogFragment extends ListFragment
registerPhoneCallReceiver();
mCallLogQueryHandler.fetchCalls(Calls.VOICEMAIL_TYPE);
updateFilterTypeAndHeader(Calls.VOICEMAIL_TYPE);
- mShowingVoicemailOnly = true;
return true;
case R.id.show_all_calls:
@@ -383,7 +415,6 @@ public class CallLogFragment extends ListFragment
unregisterPhoneCallReceiver();
mCallLogQueryHandler.fetchCalls(CallLogQueryHandler.CALL_TYPE_ALL);
updateFilterTypeAndHeader(CallLogQueryHandler.CALL_TYPE_ALL);
- mShowingVoicemailOnly = false;
return true;
default:
diff --git a/src/com/android/dialer/dialpad/DialpadFragment.java b/src/com/android/dialer/dialpad/DialpadFragment.java
index 2b77b4619..f7a9056f5 100644
--- a/src/com/android/dialer/dialpad/DialpadFragment.java
+++ b/src/com/android/dialer/dialpad/DialpadFragment.java
@@ -31,6 +31,7 @@ import android.graphics.BitmapFactory;
import android.media.AudioManager;
import android.media.ToneGenerator;
import android.net.Uri;
+import android.os.AsyncTask;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -39,6 +40,7 @@ import android.provider.Contacts.Intents.Insert;
import android.provider.Contacts.People;
import android.provider.Contacts.Phones;
import android.provider.Contacts.PhonesColumns;
+import android.provider.ContactsContract.Contacts;
import android.provider.Settings;
import android.telephony.PhoneNumberUtils;
import android.telephony.PhoneStateListener;
@@ -59,6 +61,7 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
+import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.EditText;
@@ -68,9 +71,12 @@ import android.widget.PopupMenu;
import android.widget.TextView;
import com.android.contacts.common.CallUtil;
+import com.android.contacts.common.activity.TransactionSafeActivity;
+import com.android.dialer.interactions.PhoneNumberInteraction;
import com.android.contacts.common.GeoUtil;
import com.android.contacts.common.util.PhoneNumberFormatter;
import com.android.contacts.common.util.StopWatch;
+import com.android.contacts.common.preference.ContactsPreferences;
import com.android.dialer.DialtactsActivity;
import com.android.dialer.R;
import com.android.dialer.SpecialCharSequenceMgr;
@@ -79,6 +85,9 @@ import com.android.internal.telephony.ITelephony;
import com.android.phone.common.CallLogAsync;
import com.android.phone.common.HapticFeedback;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* Fragment that displays a twelve-key phone dialpad.
*/
@@ -87,7 +96,8 @@ public class DialpadFragment extends Fragment
View.OnLongClickListener, View.OnKeyListener,
AdapterView.OnItemClickListener, TextWatcher,
PopupMenu.OnMenuItemClickListener,
- DialpadImageButton.OnPressedListener {
+ DialpadImageButton.OnPressedListener,
+ SmartDialLoaderTask.SmartDialLoaderCallback {
private static final String TAG = DialpadFragment.class.getSimpleName();
private static final boolean DEBUG = DialtactsActivity.DEBUG;
@@ -104,6 +114,8 @@ public class DialpadFragment extends Fragment
/** Stream type used to play the DTMF tones off call, and mapped to the volume control keys */
private static final int DIAL_TONE_STREAM_TYPE = AudioManager.STREAM_DTMF;
+ private ContactsPreferences mContactsPrefs;
+
/**
* View (usually FrameLayout) containing mDigits field. This can be null, in which mDigits
* isn't enclosed by the container.
@@ -131,6 +143,15 @@ public class DialpadFragment extends Fragment
private ListView mDialpadChooser;
private DialpadChooserAdapter mDialpadChooserAdapter;
+ /** Will be set only if the view has the smart dialing section. */
+ private AbsListView mSmartDialList;
+
+ /**
+ * Adapter for {@link #mSmartDialList}.
+ * Will be set only if the view has the smart dialing section.
+ */
+ private SmartDialAdapter mSmartDialAdapter;
+
/**
* Regular expression prohibiting manual phone call. Can be empty, which means "no rule".
*/
@@ -149,6 +170,7 @@ public class DialpadFragment extends Fragment
// Vibration (haptic feedback) for dialer key presses.
private final HapticFeedback mHaptic = new HapticFeedback();
+ private boolean mNeedToCacheSmartDial = false;
/** Identifier for the "Add Call" intent extra. */
private static final String ADD_CALL_MODE_KEY = "add_call_mode";
@@ -199,6 +221,8 @@ public class DialpadFragment extends Fragment
*/
private boolean mDigitsFilledByIntent;
+ private boolean mStartedFromNewIntent = false;
+
private static final String PREF_DIGITS_FILLED_BY_INTENT = "pref_digits_filled_by_intent";
/**
@@ -246,14 +270,16 @@ public class DialpadFragment extends Fragment
}
updateDialAndDeleteButtonEnabledState();
+ loadSmartDialEntries();
}
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
+ mContactsPrefs = new ContactsPreferences(getActivity());
mCurrentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
-
+ mNeedToCacheSmartDial = true;
try {
mHaptic.init(getActivity(),
getResources().getBoolean(R.bool.config_enable_dialer_key_vibration));
@@ -269,6 +295,13 @@ public class DialpadFragment extends Fragment
if (state != null) {
mDigitsFilledByIntent = state.getBoolean(PREF_DIGITS_FILLED_BY_INTENT);
}
+
+ // Start caching contacts to use for smart dialling only if the dialpad fragment is visible
+ if (getUserVisibleHint()) {
+ SmartDialLoaderTask.startCacheContactsTaskIfNeeded(
+ getActivity(), mContactsPrefs.getDisplayOrder());
+ mNeedToCacheSmartDial = false;
+ }
}
@Override
@@ -334,7 +367,13 @@ public class DialpadFragment extends Fragment
mDialpadChooser = (ListView) fragmentView.findViewById(R.id.dialpadChooser);
mDialpadChooser.setOnItemClickListener(this);
- configureScreenFromIntent(getActivity().getIntent());
+ // Smart dial
+ mSmartDialList = (AbsListView) fragmentView.findViewById(R.id.dialpad_smartdial_list);
+ if (mSmartDialList != null) {
+ mSmartDialAdapter = new SmartDialAdapter(getActivity());
+ mSmartDialList.setAdapter(mSmartDialAdapter);
+ mSmartDialList.setOnItemClickListener(new OnSmartDialItemClick());
+ }
return fragmentView;
}
@@ -392,45 +431,11 @@ public class DialpadFragment extends Fragment
}
/**
- * @see #showDialpadChooser(boolean)
+ * Determines whether an add call operation is requested.
+ *
+ * @param intent The intent.
+ * @return {@literal true} if add call operation was requested. {@literal false} otherwise.
*/
- private static boolean needToShowDialpadChooser(Intent intent, boolean isAddCallMode) {
- final String action = intent.getAction();
-
- boolean needToShowDialpadChooser = false;
-
- if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)) {
- Uri uri = intent.getData();
- if (uri == null) {
- // ACTION_DIAL or ACTION_VIEW with no data.
- // This behaves basically like ACTION_MAIN: If there's
- // already an active call, bring up an intermediate UI to
- // make the user confirm what they really want to do.
- // Be sure *not* to show the dialpad chooser if this is an
- // explicit "Add call" action, though.
- if (!isAddCallMode && phoneIsInUse()) {
- needToShowDialpadChooser = true;
- }
- }
- } else if (Intent.ACTION_MAIN.equals(action)) {
- // The MAIN action means we're bringing up a blank dialer
- // (e.g. by selecting the Home shortcut, or tabbing over from
- // Contacts or Call log.)
- //
- // At this point, IF there's already an active call, there's a
- // good chance that the user got here accidentally (but really
- // wanted the in-call dialpad instead). So we bring up an
- // intermediate UI to make the user confirm what they really
- // want to do.
- if (phoneIsInUse()) {
- // Log.i(TAG, "resolveIntent(): phone is in use; showing dialpad chooser!");
- needToShowDialpadChooser = true;
- }
- }
-
- return needToShowDialpadChooser;
- }
-
private static boolean isAddCallMode(Intent intent) {
final String action = intent.getAction();
if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)) {
@@ -445,7 +450,7 @@ public class DialpadFragment extends Fragment
* Checks the given Intent and changes dialpad's UI state. For example, if the Intent requires
* the screen to enter "Add Call" mode, this method will show correct UI for the mode.
*/
- public void configureScreenFromIntent(Intent intent) {
+ private void configureScreenFromIntent(Intent intent) {
if (!isLayoutReady()) {
// This happens typically when parent's Activity#onNewIntent() is called while
// Fragment#onCreateView() isn't called yet, and thus we cannot configure Views at
@@ -458,16 +463,37 @@ public class DialpadFragment extends Fragment
boolean needToShowDialpadChooser = false;
+ // Be sure *not* to show the dialpad chooser if this is an
+ // explicit "Add call" action, though.
final boolean isAddCallMode = isAddCallMode(intent);
if (!isAddCallMode) {
+
+ // Don't show the chooser when called via onNewIntent() and phone number is present.
+ // i.e. User clicks a telephone link from gmail for example.
+ // In this case, we want to show the dialpad with the phone number.
final boolean digitsFilled = fillDigitsIfNecessary(intent);
- if (!digitsFilled) {
- needToShowDialpadChooser = needToShowDialpadChooser(intent, isAddCallMode);
+ if (!(mStartedFromNewIntent && digitsFilled)) {
+
+ final String action = intent.getAction();
+ if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)
+ || Intent.ACTION_MAIN.equals(action)) {
+ // If there's already an active call, bring up an intermediate UI to
+ // make the user confirm what they really want to do.
+ if (phoneIsInUse()) {
+ needToShowDialpadChooser = true;
+ }
+ }
+
}
}
+
showDialpadChooser(needToShowDialpadChooser);
}
+ public void setStartedFromNewIntent(boolean value) {
+ mStartedFromNewIntent = value;
+ }
+
/**
* Sets formatted digits to digits field.
*/
@@ -501,6 +527,13 @@ public class DialpadFragment extends Fragment
}
@Override
+ public void onStart() {
+ super.onStart();
+ configureScreenFromIntent(getActivity().getIntent());
+ setStartedFromNewIntent(false);
+ }
+
+ @Override
public void onResume() {
super.onResume();
@@ -1115,6 +1148,11 @@ public class DialpadFragment extends Fragment
}
}
+ private String getCallOrigin() {
+ return (getActivity() instanceof DialtactsActivity) ?
+ ((DialtactsActivity) getActivity()).getCallOrigin() : null;
+ }
+
private void handleDialButtonClickWithEmptyDigits() {
if (phoneIsCdma() && phoneIsOffhook()) {
// This is really CDMA specific. On GSM is it possible
@@ -1637,4 +1675,59 @@ public class DialpadFragment extends Fragment
intent.putExtra(EXTRA_SEND_EMPTY_FLASH, true);
return intent;
}
+
+ @Override
+ public void setUserVisibleHint(boolean isVisibleToUser) {
+ super.setUserVisibleHint(isVisibleToUser);
+ if (isVisibleToUser && mNeedToCacheSmartDial) {
+ SmartDialLoaderTask.startCacheContactsTaskIfNeeded(
+ getActivity(), mContactsPrefs.getDisplayOrder());
+ mNeedToCacheSmartDial = false;
+ }
+ }
+
+ private String mLastDigitsForSmartDial;
+
+ private void loadSmartDialEntries() {
+ if (mSmartDialAdapter == null) {
+ // No smart dial views. Landscape?
+ return;
+ }
+
+ // Update only when the digits have changed.
+ final String digits = SmartDialNameMatcher.normalizeNumber(mDigits.getText().toString());
+ if (TextUtils.equals(digits, mLastDigitsForSmartDial)) {
+ return;
+ }
+ mLastDigitsForSmartDial = digits;
+
+ if (digits.length() < 2) {
+ mSmartDialAdapter.clear();
+ } else {
+ final SmartDialLoaderTask task = new SmartDialLoaderTask(this, digits);
+ // don't execute this in serial, otherwise we have to wait too long for results
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new String[] {});
+ }
+ }
+
+ @Override
+ public void setSmartDialAdapterEntries(List<SmartDialEntry> data) {
+ if (data == null || data.isEmpty()) {
+ // No results found. Keep the last results.
+ return;
+ }
+ mSmartDialAdapter.setEntries(data);
+ }
+
+ private class OnSmartDialItemClick implements AdapterView.OnItemClickListener {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ final SmartDialEntry entry = (SmartDialEntry) view.getTag();
+ if (entry == null) return; // just in case.
+
+ mClearDigitsOnStop = true;
+ PhoneNumberInteraction.startInteractionForPhoneCall(
+ (TransactionSafeActivity) getActivity(), entry.contactUri, getCallOrigin());
+ }
+ }
}
diff --git a/src/com/android/dialer/dialpad/SmartDialAdapter.java b/src/com/android/dialer/dialpad/SmartDialAdapter.java
new file mode 100644
index 000000000..9330909e7
--- /dev/null
+++ b/src/com/android/dialer/dialpad/SmartDialAdapter.java
@@ -0,0 +1,169 @@
+/*
+ * 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.dialpad;
+
+import com.android.contacts.R;
+import com.google.common.collect.Lists;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.CharacterStyle;
+import android.text.style.ForegroundColorSpan;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+import java.util.List;
+
+public class SmartDialAdapter extends BaseAdapter {
+ public static final String LOG_TAG = "SmartDial";
+ private final LayoutInflater mInflater;
+
+ private List<SmartDialEntry> mEntries;
+ private static Drawable mHighConfidenceHint;
+
+ private final int mHighlightedTextColor;
+
+ public SmartDialAdapter(Context context) {
+ mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ final Resources res = context.getResources();
+ mHighConfidenceHint = SmartDialTextView.getHighConfidenceHintDrawable(
+ res, res.getDimension(R.dimen.smartdial_confidence_hint_text_size),
+ res.getColor(R.color.smartdial_confidence_drawable_color));
+ mHighlightedTextColor = res.getColor(R.color.smartdial_highlighted_text_color);
+ clear();
+ }
+
+ /** Remove all entries. */
+ public void clear() {
+ mEntries = Lists.newArrayList();
+ notifyDataSetChanged();
+ }
+
+ /** Set entries. */
+ public void setEntries(List<SmartDialEntry> entries) {
+ if (entries == null) throw new IllegalArgumentException();
+ mEntries = entries;
+
+ if (mEntries.size() <= 1) {
+ // add a null entry to push the single entry into the middle
+ mEntries.add(0, null);
+ } else if (mEntries.size() >= 2){
+ // swap the 1st and 2nd entries so that the highest confidence match goes into the
+ // middle
+ final SmartDialEntry temp = mEntries.get(0);
+ mEntries.set(0, mEntries.get(1));
+ mEntries.set(1, temp);
+ }
+
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return !(mEntries.get(position) == null);
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return false;
+ }
+
+ @Override
+ public int getCount() {
+ return mEntries.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mEntries.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position; // Just use the position as the ID, so it's not stable.
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return false; // Not stable because we just use the position as the ID.
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final SmartDialTextView view;
+ if (convertView == null) {
+ view = (SmartDialTextView) mInflater.inflate(
+ R.layout.dialpad_smartdial_item, parent, false);
+ } else {
+ view = (SmartDialTextView) convertView;
+ }
+ // Set the display name with highlight.
+
+ final SmartDialEntry item = mEntries.get(position);
+
+ if (item == null) {
+ // Clear the text in case the view was reused.
+ view.setText("");
+ // Empty view. We use this to force a single entry to be in the middle
+ return view;
+ }
+ final SpannableString displayName = new SpannableString(item.displayName);
+ for (final SmartDialMatchPosition p : item.matchPositions) {
+ final int matchStart = p.start;
+ final int matchEnd = p.end;
+ if (matchStart < matchEnd) {
+ // Create a new ForegroundColorSpan for each section of the name to highlight,
+ // otherwise multiple highlights won't work.
+ try {
+ displayName.setSpan(
+ new ForegroundColorSpan(mHighlightedTextColor), matchStart, matchEnd,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } catch (final IndexOutOfBoundsException e) {
+ Log.wtf(LOG_TAG,
+ "Invalid match positions provided - [" + matchStart + ","
+ + matchEnd + "] for display name: " + item.displayName);
+ }
+ }
+ }
+
+ if (position == 1) {
+ view.setCompoundDrawablesWithIntrinsicBounds(
+ null, null, null, mHighConfidenceHint);
+ // Hack to align text in this view with text in other views without the
+ // overflow drawable
+ view.setCompoundDrawablePadding(-mHighConfidenceHint.getIntrinsicHeight());
+ } else {
+ view.setCompoundDrawablesWithIntrinsicBounds(
+ null, null, null, null);
+ }
+
+
+ view.setText(displayName);
+ view.setTag(item);
+
+ return view;
+ }
+}
diff --git a/src/com/android/dialer/dialpad/SmartDialEntry.java b/src/com/android/dialer/dialpad/SmartDialEntry.java
new file mode 100644
index 000000000..101678de0
--- /dev/null
+++ b/src/com/android/dialer/dialpad/SmartDialEntry.java
@@ -0,0 +1,36 @@
+/*
+ * 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.dialpad;
+
+import android.net.Uri;
+
+import java.util.ArrayList;
+
+public class SmartDialEntry {
+ /** Display name for the contact. */
+ public final CharSequence displayName;
+ public final Uri contactUri;
+
+ public final ArrayList<SmartDialMatchPosition> matchPositions;
+
+ public SmartDialEntry(CharSequence displayName, Uri contactUri,
+ ArrayList<SmartDialMatchPosition> matchPositions) {
+ this.displayName = displayName;
+ this.contactUri = contactUri;
+ this.matchPositions = matchPositions;
+ }
+}
diff --git a/src/com/android/dialer/dialpad/SmartDialLoaderTask.java b/src/com/android/dialer/dialpad/SmartDialLoaderTask.java
new file mode 100644
index 000000000..832183785
--- /dev/null
+++ b/src/com/android/dialer/dialpad/SmartDialLoaderTask.java
@@ -0,0 +1,254 @@
+/*
+ * 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.dialpad;
+
+import static com.android.dialer.dialpad.SmartDialAdapter.LOG_TAG;
+
+import com.google.common.collect.Lists;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.telephony.PhoneNumberUtils;
+import android.util.Log;
+
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.common.util.StopWatch;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * AsyncTask that performs one of two functions depending on which constructor is used.
+ * If {@link #SmartDialLoaderTask(Context context, int nameDisplayOrder)} is used, the task
+ * caches all contacts with a phone number into the static variable {@link #sContactsCache}.
+ * If {@link #SmartDialLoaderTask(SmartDialLoaderCallback callback, String query)} is used, the
+ * task searches through the cache to return the top 3 contacts(ranked by confidence) that match
+ * the query, then passes it back to the {@link SmartDialLoaderCallback} through a callback
+ * function.
+ */
+// TODO: Make the cache a singleton class and refactor to fix possible concurrency issues in the
+// future
+public class SmartDialLoaderTask extends AsyncTask<String, Integer, List<SmartDialEntry>> {
+
+ private class Contact {
+ final String mDisplayName;
+ final String mLookupKey;
+ final long mId;
+
+ public Contact(long id, String displayName, String lookupKey) {
+ mDisplayName = displayName;
+ mLookupKey = lookupKey;
+ mId = id;
+ }
+ }
+
+ public interface SmartDialLoaderCallback {
+ void setSmartDialAdapterEntries(List<SmartDialEntry> list);
+ }
+
+ static private final boolean DEBUG = true; // STOPSHIP change to false.
+
+ private static final int MAX_ENTRIES = 3;
+
+ private static List<Contact> sContactsCache;
+
+ private final boolean mCacheOnly;
+
+ private final SmartDialLoaderCallback mCallback;
+
+ private final Context mContext;
+ /**
+ * See {@link ContactsPreferences#getDisplayOrder()}.
+ * {@link ContactsContract.Preferences#DISPLAY_ORDER_PRIMARY} (first name first)
+ * {@link ContactsContract.Preferences#DISPLAY_ORDER_ALTERNATIVE} (last name first)
+ */
+ private final int mNameDisplayOrder;
+
+ private final SmartDialNameMatcher mNameMatcher;
+
+ // cache only constructor
+ private SmartDialLoaderTask(Context context, int nameDisplayOrder) {
+ this.mNameDisplayOrder = nameDisplayOrder;
+ this.mContext = context;
+ // we're just caching contacts so no need to initialize a SmartDialNameMatcher or callback
+ this.mNameMatcher = null;
+ this.mCallback = null;
+ this.mCacheOnly = true;
+ }
+
+ public SmartDialLoaderTask(SmartDialLoaderCallback callback, String query) {
+ this.mCallback = callback;
+ this.mContext = null;
+ this.mCacheOnly = false;
+ this.mNameDisplayOrder = 0;
+ this.mNameMatcher = new SmartDialNameMatcher(PhoneNumberUtils.normalizeNumber(query));
+ }
+
+ @Override
+ protected List<SmartDialEntry> doInBackground(String... params) {
+ if (mCacheOnly) {
+ cacheContacts();
+ return Lists.newArrayList();
+ }
+
+ return getContactMatches();
+ }
+
+ @Override
+ protected void onPostExecute(List<SmartDialEntry> result) {
+ if (mCallback != null) {
+ mCallback.setSmartDialAdapterEntries(result);
+ }
+ }
+
+ /** Query used for loadByContactName */
+ private interface ContactQuery {
+ Uri URI = Contacts.CONTENT_URI.buildUpon()
+ // Visible contact only
+ //.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, "0")
+ .build();
+ String[] PROJECTION = new String[] {
+ Contacts._ID,
+ Contacts.DISPLAY_NAME,
+ Contacts.LOOKUP_KEY
+ };
+ String[] PROJECTION_ALTERNATIVE = new String[] {
+ Contacts._ID,
+ Contacts.DISPLAY_NAME_ALTERNATIVE,
+ Contacts.LOOKUP_KEY
+ };
+
+ int COLUMN_ID = 0;
+ int COLUMN_DISPLAY_NAME = 1;
+ int COLUMN_LOOKUP_KEY = 2;
+
+ String SELECTION =
+ //Contacts.IN_VISIBLE_GROUP + "=1 and " +
+ Contacts.HAS_PHONE_NUMBER + "=1";
+
+ String ORDER_BY = Contacts.LAST_TIME_CONTACTED + " DESC";
+ }
+
+ public static void startCacheContactsTaskIfNeeded(Context context, int displayOrder) {
+ if (sContactsCache != null) {
+ // contacts have already been cached, just return
+ return;
+ }
+ final SmartDialLoaderTask task =
+ new SmartDialLoaderTask(context, displayOrder);
+ task.execute();
+ }
+
+ /**
+ * Caches the contacts into an in memory array list. This is called once at startup and should
+ * not be cancelled.
+ */
+ private void cacheContacts() {
+ final StopWatch stopWatch = DEBUG ? StopWatch.start("SmartDial Cache") : null;
+ if (sContactsCache != null) {
+ // contacts have already been cached, just return
+ stopWatch.stopAndLog("SmartDial Already Cached", 0);
+ return;
+ }
+ if (mContext == null) {
+ if (DEBUG) {
+ stopWatch.stopAndLog("Invalid context", 0);
+ }
+ return;
+ }
+ final Cursor c = mContext.getContentResolver().query(ContactQuery.URI,
+ (mNameDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY)
+ ? ContactQuery.PROJECTION : ContactQuery.PROJECTION_ALTERNATIVE,
+ ContactQuery.SELECTION, null,
+ ContactQuery.ORDER_BY);
+ if (c == null) {
+ if (DEBUG) {
+ stopWatch.stopAndLog("Query Failure", 0);
+ }
+ return;
+ }
+ sContactsCache = Lists.newArrayListWithCapacity(c.getCount());
+ try {
+ c.moveToPosition(-1);
+ while (c.moveToNext()) {
+ final String displayName = c.getString(ContactQuery.COLUMN_DISPLAY_NAME);
+ final long id = c.getLong(ContactQuery.COLUMN_ID);
+ final String lookupKey = c.getString(ContactQuery.COLUMN_LOOKUP_KEY);
+ sContactsCache.add(new Contact(id, displayName, lookupKey));
+ }
+ } finally {
+ c.close();
+ if (DEBUG) {
+ stopWatch.stopAndLog("SmartDial Cache", 0);
+ }
+ }
+ }
+
+ /**
+ * Loads all visible contacts with phone numbers and check if their display names match the
+ * query. Return at most {@link #MAX_ENTRIES} {@link SmartDialEntry}'s for the matching
+ * contacts.
+ */
+ private ArrayList<SmartDialEntry> getContactMatches() {
+ final StopWatch stopWatch = DEBUG ? StopWatch.start(LOG_TAG + " Start Match") : null;
+ if (sContactsCache == null) {
+ // contacts should have been cached by this point in time, but in case they
+ // are not, we go ahead and cache them into memory.
+ if (DEBUG) {
+ Log.d(LOG_TAG, "empty cache");
+ }
+ cacheContacts();
+ // TODO: if sContactsCache is still null at this point we should try to recache
+ }
+ if (DEBUG) {
+ Log.d(LOG_TAG, "Size of cache: " + sContactsCache.size());
+ }
+ final ArrayList<SmartDialEntry> outList = Lists.newArrayList();
+ if (sContactsCache == null) {
+ return outList;
+ }
+ int count = 0;
+ for (int i = 0; i < sContactsCache.size(); i++) {
+ final Contact contact = sContactsCache.get(i);
+ final String displayName = contact.mDisplayName;
+
+ if (!mNameMatcher.matches(displayName)) {
+ continue;
+ }
+ // Matched; create SmartDialEntry.
+ @SuppressWarnings("unchecked")
+ final SmartDialEntry entry = new SmartDialEntry(
+ contact.mDisplayName,
+ Contacts.getLookupUri(contact.mId, contact.mLookupKey),
+ (ArrayList<SmartDialMatchPosition>) mNameMatcher.getMatchPositions().clone()
+ );
+ outList.add(entry);
+ count++;
+ if (count >= MAX_ENTRIES) {
+ break;
+ }
+ }
+ if (DEBUG) {
+ stopWatch.stopAndLog(LOG_TAG + " Match Complete", 0);
+ }
+ return outList;
+ }
+}
diff --git a/src/com/android/dialer/dialpad/SmartDialMatchPosition.java b/src/com/android/dialer/dialpad/SmartDialMatchPosition.java
new file mode 100644
index 000000000..3d248ccb2
--- /dev/null
+++ b/src/com/android/dialer/dialpad/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.dialpad;
+
+import static com.android.dialer.dialpad.SmartDialAdapter.LOG_TAG;
+
+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 by {@link SmartDialAdapter} to highlight certain parts of the contact's
+ * display name to indicate that those ranges matched the user's query.
+ */
+class SmartDialMatchPosition {
+ public int start;
+ public int end;
+
+ public SmartDialMatchPosition(int start, int end) {
+ this.start = start;
+ this.end = end;
+ }
+
+ private void advance(int toAdvance) {
+ this.start += toAdvance;
+ this.end += toAdvance;
+ }
+
+ /**
+ * 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<SmartDialMatchPosition> 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<SmartDialMatchPosition> list) {
+ for (int i = 0; i < list.size(); i ++) {
+ SmartDialMatchPosition m = list.get(i);
+ Log.d(LOG_TAG, "[" + m.start + "," + m.end + "]");
+ }
+ }
+}
diff --git a/src/com/android/dialer/dialpad/SmartDialNameMatcher.java b/src/com/android/dialer/dialpad/SmartDialNameMatcher.java
new file mode 100644
index 000000000..1253a566d
--- /dev/null
+++ b/src/com/android/dialer/dialpad/SmartDialNameMatcher.java
@@ -0,0 +1,552 @@
+/*
+ * 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.dialpad;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+
+import com.android.contacts.test.NeededForTesting;
+
+import java.text.Normalizer;
+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 {
+
+ private final String mQuery;
+
+ private static final char[] LETTERS_TO_DIGITS = {
+ '2', '2', '2', // A,B,C -> 2
+ '3', '3', '3', // D,E,F -> 3
+ '4', '4', '4', // G,H,I -> 4
+ '5', '5', '5', // J,K,L -> 5
+ '6', '6', '6', // M,N,O -> 6
+ '7', '7', '7', '7', // P,Q,R,S -> 7
+ '8', '8', '8', // T,U,V -> 8
+ '9', '9', '9', '9' // W,X,Y,Z -> 9
+ };
+
+ /*
+ * 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 '" + 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
+ */
+ private static char remapAccentedChars(char c) {
+ switch (c) {
+ case 'À': return 'A';
+ case 'Á': return 'A';
+ case 'Â': return 'A';
+ case 'Ã': return 'A';
+ case 'Ä': return 'A';
+ case 'Å': return 'A';
+ case 'Ç': return 'C';
+ case 'È': return 'E';
+ case 'É': return 'E';
+ case 'Ê': return 'E';
+ case 'Ë': return 'E';
+ case 'Ì': return 'I';
+ case 'Í': return 'I';
+ case 'Î': return 'I';
+ case 'Ï': return 'I';
+ case 'Ð': return 'D';
+ case 'Ñ': return 'N';
+ case 'Ò': return 'O';
+ case 'Ó': return 'O';
+ case 'Ô': return 'O';
+ case 'Õ': return 'O';
+ case 'Ö': return 'O';
+ case '×': return 'x';
+ case 'Ø': return 'O';
+ case 'Ù': return 'U';
+ case 'Ú': return 'U';
+ case 'Û': return 'U';
+ case 'Ü': return 'U';
+ case 'Ý': return 'U';
+ case 'à': return 'a';
+ case 'á': return 'a';
+ case 'â': return 'a';
+ case 'ã': return 'a';
+ case 'ä': return 'a';
+ case 'å': return 'a';
+ case 'ç': return 'c';
+ case 'è': return 'e';
+ case 'é': return 'e';
+ case 'ê': return 'e';
+ case 'ë': return 'e';
+ case 'ì': return 'i';
+ case 'í': return 'i';
+ case 'î': return 'i';
+ case 'ï': return 'i';
+ case 'ð': return 'd';
+ case 'ñ': return 'n';
+ case 'ò': return 'o';
+ case 'ó': return 'o';
+ case 'ô': return 'o';
+ case 'õ': return 'o';
+ case 'ö': return 'o';
+ case 'ø': return 'o';
+ case 'ù': return 'u';
+ case 'ú': return 'u';
+ case 'û': return 'u';
+ case 'ü': return 'u';
+ case 'ý': return 'y';
+ case 'ÿ': return 'y';
+ case 'Ā': return 'A';
+ case 'ā': return 'a';
+ case 'Ă': return 'A';
+ case 'ă': return 'a';
+ case 'Ą': return 'A';
+ case 'ą': return 'a';
+ case 'Ć': return 'C';
+ case 'ć': return 'c';
+ case 'Ĉ': return 'C';
+ case 'ĉ': return 'c';
+ case 'Ċ': return 'C';
+ case 'ċ': return 'c';
+ case 'Č': return 'C';
+ case 'č': return 'c';
+ case 'Ď': return 'D';
+ case 'ď': return 'd';
+ case 'Đ': return 'D';
+ case 'đ': return 'd';
+ case 'Ē': return 'E';
+ case 'ē': return 'e';
+ case 'Ĕ': return 'E';
+ case 'ĕ': return 'e';
+ case 'Ė': return 'E';
+ case 'ė': return 'e';
+ case 'Ę': return 'E';
+ case 'ę': return 'e';
+ case 'Ě': return 'E';
+ case 'ě': return 'e';
+ case 'Ĝ': return 'G';
+ case 'ĝ': return 'g';
+ case 'Ğ': return 'G';
+ case 'ğ': return 'g';
+ case 'Ġ': return 'G';
+ case 'ġ': return 'g';
+ case 'Ģ': return 'G';
+ case 'ģ': return 'g';
+ case 'Ĥ': return 'H';
+ case 'ĥ': return 'h';
+ case 'Ħ': return 'H';
+ case 'ħ': return 'h';
+ case 'Ĩ': return 'I';
+ case 'ĩ': return 'i';
+ case 'Ī': return 'I';
+ case 'ī': return 'i';
+ case 'Ĭ': return 'I';
+ case 'ĭ': return 'i';
+ case 'Į': return 'I';
+ case 'į': return 'i';
+ case 'İ': return 'I';
+ case 'ı': return 'i';
+ case 'Ĵ': return 'J';
+ case 'ĵ': return 'j';
+ case 'Ķ': return 'K';
+ case 'ķ': return 'k';
+ case 'ĸ': return 'k';
+ case 'Ĺ': return 'L';
+ case 'ĺ': return 'l';
+ case 'Ļ': return 'L';
+ case 'ļ': return 'l';
+ case 'Ľ': return 'L';
+ case 'ľ': return 'l';
+ case 'Ŀ': return 'L';
+ case 'ŀ': return 'l';
+ case 'Ł': return 'L';
+ case 'ł': return 'l';
+ case 'Ń': return 'N';
+ case 'ń': return 'n';
+ case 'Ņ': return 'N';
+ case 'ņ': return 'n';
+ case 'Ň': return 'N';
+ case 'ň': return 'n';
+ case 'Ō': return 'O';
+ case 'ō': return 'o';
+ case 'Ŏ': return 'O';
+ case 'ŏ': return 'o';
+ case 'Ő': return 'O';
+ case 'ő': return 'o';
+ case 'Ŕ': return 'R';
+ case 'ŕ': return 'r';
+ case 'Ŗ': return 'R';
+ case 'ŗ': return 'r';
+ case 'Ř': return 'R';
+ case 'ř': return 'r';
+ case 'Ś': return 'S';
+ case 'ś': return 's';
+ case 'Ŝ': return 'S';
+ case 'ŝ': return 's';
+ case 'Ş': return 'S';
+ case 'ş': return 's';
+ case 'Š': return 'S';
+ case 'š': return 's';
+ case 'Ţ': return 'T';
+ case 'ţ': return 't';
+ case 'Ť': return 'T';
+ case 'ť': return 't';
+ case 'Ŧ': return 'T';
+ case 'ŧ': return 't';
+ case 'Ũ': return 'U';
+ case 'ũ': return 'u';
+ case 'Ū': return 'U';
+ case 'ū': return 'u';
+ case 'Ŭ': return 'U';
+ case 'ŭ': return 'u';
+ case 'Ů': return 'U';
+ case 'ů': return 'u';
+ case 'Ű': return 'U';
+ case 'ű': return 'u';
+ case 'Ų': return 'U';
+ case 'ų': return 'u';
+ case 'Ŵ': return 'W';
+ case 'ŵ': return 'w';
+ case 'Ŷ': return 'Y';
+ case 'ŷ': return 'y';
+ case 'Ÿ': return 'Y';
+ case 'Ź': return 'Z';
+ case 'ź': return 'z';
+ case 'Ż': return 'Z';
+ case 'ż': return 'z';
+ case 'Ž': return 'Z';
+ case 'ž': return 'z';
+ case 'ſ': return 's';
+ case 'ƀ': return 'b';
+ case 'Ɓ': return 'B';
+ case 'Ƃ': return 'B';
+ case 'ƃ': return 'b';
+ case 'Ɔ': return 'O';
+ case 'Ƈ': return 'C';
+ case 'ƈ': return 'c';
+ case 'Ɖ': return 'D';
+ case 'Ɗ': return 'D';
+ case 'Ƌ': return 'D';
+ case 'ƌ': return 'd';
+ case 'ƍ': return 'd';
+ case 'Ɛ': return 'E';
+ case 'Ƒ': return 'F';
+ case 'ƒ': return 'f';
+ case 'Ɠ': return 'G';
+ case 'Ɣ': return 'G';
+ case 'Ɩ': return 'I';
+ case 'Ɨ': return 'I';
+ case 'Ƙ': return 'K';
+ case 'ƙ': return 'k';
+ case 'ƚ': return 'l';
+ case 'ƛ': return 'l';
+ case 'Ɯ': return 'W';
+ case 'Ɲ': return 'N';
+ case 'ƞ': return 'n';
+ case 'Ɵ': return 'O';
+ case 'Ơ': return 'O';
+ case 'ơ': return 'o';
+ case 'Ƥ': return 'P';
+ case 'ƥ': return 'p';
+ case 'ƫ': return 't';
+ case 'Ƭ': return 'T';
+ case 'ƭ': return 't';
+ case 'Ʈ': return 'T';
+ case 'Ư': return 'U';
+ case 'ư': return 'u';
+ case 'Ʊ': return 'Y';
+ case 'Ʋ': return 'V';
+ case 'Ƴ': return 'Y';
+ case 'ƴ': return 'y';
+ case 'Ƶ': return 'Z';
+ case 'ƶ': return 'z';
+ case 'ƿ': return 'w';
+ case 'Ǎ': return 'A';
+ case 'ǎ': return 'a';
+ case 'Ǐ': return 'I';
+ case 'ǐ': return 'i';
+ case 'Ǒ': return 'O';
+ case 'ǒ': return 'o';
+ case 'Ǔ': return 'U';
+ case 'ǔ': return 'u';
+ case 'Ǖ': return 'U';
+ case 'ǖ': return 'u';
+ case 'Ǘ': return 'U';
+ case 'ǘ': return 'u';
+ case 'Ǚ': return 'U';
+ case 'ǚ': return 'u';
+ case 'Ǜ': return 'U';
+ case 'ǜ': return 'u';
+ case 'Ǟ': return 'A';
+ case 'ǟ': return 'a';
+ case 'Ǡ': return 'A';
+ case 'ǡ': return 'a';
+ case 'Ǥ': return 'G';
+ case 'ǥ': return 'g';
+ case 'Ǧ': return 'G';
+ case 'ǧ': return 'g';
+ case 'Ǩ': return 'K';
+ case 'ǩ': return 'k';
+ case 'Ǫ': return 'O';
+ case 'ǫ': return 'o';
+ case 'Ǭ': return 'O';
+ case 'ǭ': return 'o';
+ case 'ǰ': return 'j';
+ case 'Dz': return 'D';
+ case 'Ǵ': return 'G';
+ case 'ǵ': return 'g';
+ case 'Ƿ': return 'W';
+ case 'Ǹ': return 'N';
+ case 'ǹ': return 'n';
+ case 'Ǻ': return 'A';
+ case 'ǻ': return 'a';
+ case 'Ǿ': return 'O';
+ case 'ǿ': return 'o';
+ case 'Ȁ': return 'A';
+ case 'ȁ': return 'a';
+ case 'Ȃ': return 'A';
+ case 'ȃ': return 'a';
+ case 'Ȅ': return 'E';
+ case 'ȅ': return 'e';
+ case 'Ȇ': return 'E';
+ case 'ȇ': return 'e';
+ case 'Ȉ': return 'I';
+ case 'ȉ': return 'i';
+ case 'Ȋ': return 'I';
+ case 'ȋ': return 'i';
+ case 'Ȍ': return 'O';
+ case 'ȍ': return 'o';
+ case 'Ȏ': return 'O';
+ case 'ȏ': return 'o';
+ case 'Ȑ': return 'R';
+ case 'ȑ': return 'r';
+ case 'Ȓ': return 'R';
+ case 'ȓ': return 'r';
+ case 'Ȕ': return 'U';
+ case 'ȕ': return 'u';
+ case 'Ȗ': return 'U';
+ case 'ȗ': return 'u';
+ case 'Ș': return 'S';
+ case 'ș': return 's';
+ case 'Ț': return 'T';
+ case 'ț': return 't';
+ case 'Ȝ': return 'Y';
+ case 'ȝ': return 'y';
+ case 'Ȟ': return 'H';
+ case 'ȟ': return 'h';
+ case 'Ȥ': return 'Z';
+ case 'ȥ': return 'z';
+ case 'Ȧ': return 'A';
+ case 'ȧ': return 'a';
+ case 'Ȩ': return 'E';
+ case 'ȩ': return 'e';
+ case 'Ȫ': return 'O';
+ case 'ȫ': return 'o';
+ case 'Ȭ': return 'O';
+ case 'ȭ': return 'o';
+ case 'Ȯ': return 'O';
+ case 'ȯ': return 'o';
+ case 'Ȱ': return 'O';
+ case 'ȱ': return 'o';
+ case 'Ȳ': return 'Y';
+ case 'ȳ': return 'y';
+ default: return c;
+ }
+ }
+
+ private final ArrayList<SmartDialMatchPosition> mMatchPositions = Lists.newArrayList();
+
+ public SmartDialNameMatcher(String query) {
+ mQuery = query;
+ }
+
+ /**
+ * Strips all accented characters in a name and converts them to their alphabetic equivalents.
+ *
+ * @param name Name we want to remove accented characters from.
+ * @return Name without accents in characters
+ */
+ public static String stripDiacritics(String name) {
+ // NFD stands for normalization form D - Canonical Decomposition
+ // This means that for all characters with diacritics, e.g. ä, we decompose them into
+ // two characters, the first being the alphabetic equivalent, and the second being a
+ // a character that represents the diacritic.
+
+ final String normalized = Normalizer.normalize(name, Normalizer.Form.NFD);
+ final StringBuilder stripped = new StringBuilder();
+ for (int i = 0; i < normalized.length(); i++) {
+ // This pass through the string strips out all the diacritics by checking to see
+ // if they are in this list here:
+ // http://www.fileformat.info/info/unicode/category/Mn/list.htm
+ if (Character.getType(normalized.charAt(i)) != Character.NON_SPACING_MARK) {
+ stripped.append(normalized.charAt(i));
+ }
+ }
+ return stripped.toString();
+ }
+
+ /**
+ * Strips a phone number of unnecessary characters (zeros, ones, spaces, dashes, etc.)
+ *
+ * @param number Phone number we want to normalize
+ * @return Phone number consisting of digits from 2-9
+ */
+ public static String normalizeNumber(String number) {
+ final StringBuilder s = new StringBuilder();
+ for (int i = 0; i < number.length(); i++) {
+ char ch = number.charAt(i);
+ if (ch >= '2' && ch <= '9') {
+ s.append(ch);
+ }
+ }
+ return s.toString();
+ }
+
+ /**
+ * 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 whitespace. 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. For now, matchList will contain a maximum of one match
+ * position. If we intend to support initial matching in the future, matchList could possibly
+ * contain more than one match position.
+ */
+ @VisibleForTesting
+ boolean matchesCombination(String displayName, String query,
+ ArrayList<SmartDialMatchPosition> matchList) {
+ 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<SmartDialMatchPosition> partial = new ArrayList<SmartDialMatchPosition>();
+
+ // 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 = remapAccentedChars(ch);
+ if ((ch >= 'A') && (ch <= 'Z')) {
+ // Simply change the ascii code to the lower case version instead of using
+ // toLowerCase for efficiency
+ ch += 32;
+ }
+ if ((ch >= 'a') && (ch <= 'z')) {
+ // a starts at index 0
+ if (LETTERS_TO_DIGITS[ch - 'a'] != query.charAt(queryStart)) {
+ // we did not find a match
+ queryStart = 0;
+ seperatorCount = 0;
+ while (nameStart < nameLength &&
+ !Character.isWhitespace(displayName.charAt(nameStart))) {
+ nameStart++;
+ }
+ nameStart++;
+ 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));
+ return true;
+ }
+ 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++;
+ }
+ }
+ }
+ return false;
+ }
+
+ public boolean matches(String displayName) {
+ mMatchPositions.clear();
+ return matchesCombination(displayName, mQuery, mMatchPositions);
+ }
+
+ public ArrayList<SmartDialMatchPosition> getMatchPositions() {
+ return mMatchPositions;
+ }
+
+ public String getQuery() {
+ return mQuery;
+ }
+}
diff --git a/src/com/android/dialer/dialpad/SmartDialTextView.java b/src/com/android/dialer/dialpad/SmartDialTextView.java
new file mode 100644
index 000000000..b9df48ff0
--- /dev/null
+++ b/src/com/android/dialer/dialpad/SmartDialTextView.java
@@ -0,0 +1,103 @@
+/*
+ * 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.dialpad;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Paint.Align;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.widget.TextView;
+
+import com.android.dialer.R;
+
+public class SmartDialTextView extends TextView {
+
+ private final float mPadding;
+ private final float mExtraPadding;
+ private static final String HIGH_CONFIDENCE_HINT = "\u2026";
+
+ public SmartDialTextView(Context context) {
+ this(context, null);
+ }
+
+ public SmartDialTextView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mPadding = getResources().getDimension(R.dimen.smartdial_suggestions_padding);
+ mExtraPadding = getResources().getDimension(R.dimen.smartdial_suggestions_extra_padding);
+ }
+
+ /**
+ * Returns a drawable that resembles a sideways overflow icon. Used to indicate the presence
+ * of a high confidence match.
+ *
+ * @param res Resources that we will use to create our BitmapDrawable with
+ * @param textSize Size of drawable to create
+ * @param color Color of drawable to create
+ * @return The drawable drawn according to the given parameters
+ */
+ public static Drawable getHighConfidenceHintDrawable(final Resources res, final float textSize,
+ final int color) {
+ final Paint paint = new Paint();
+ paint.setAntiAlias(true);
+ paint.setTextAlign(Align.CENTER);
+ paint.setTextSize(textSize);
+ paint.setColor(color);
+ final Rect bounds = new Rect();
+ paint.getTextBounds(HIGH_CONFIDENCE_HINT, 0, HIGH_CONFIDENCE_HINT.length(), bounds);
+ final int width = bounds.width();
+ final int height = bounds.height();
+ final Bitmap buffer = Bitmap.createBitmap(
+ width, (height * 3 / 2), Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(buffer);
+ canvas.drawText(HIGH_CONFIDENCE_HINT, width / 2, height, paint);
+ return new BitmapDrawable(res, buffer);
+ }
+
+ @Override
+ protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
+ super.onTextChanged(text, start, lengthBefore, lengthAfter);
+ rescaleText(getWidth());
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ rescaleText(w);
+ }
+
+ private void rescaleText(int w) {
+ if (w == 0) {
+ return;
+ }
+ setTextScaleX(1);
+ final Paint paint = getPaint();
+ float width = w - 2 * mPadding - 2 * mExtraPadding;
+
+ float ratio = width / paint.measureText(getText().toString());
+ if (ratio < 1.0f) {
+ setTextScaleX(ratio);
+ }
+ }
+}
diff --git a/tests/src/com/android/dialer/SmartDialNameMatcherTest.java b/tests/src/com/android/dialer/SmartDialNameMatcherTest.java
new file mode 100644
index 000000000..492e5b422
--- /dev/null
+++ b/tests/src/com/android/dialer/SmartDialNameMatcherTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.dialpad;
+
+import android.test.suitebuilder.annotation.SmallTest;
+import android.test.suitebuilder.annotation.Suppress;
+import android.util.Log;
+
+import com.android.dialer.dialpad.SmartDialNameMatcher;
+
+import java.text.Normalizer;
+import java.util.ArrayList;
+
+import junit.framework.TestCase;
+
+@SmallTest
+public class SmartDialNameMatcherTest extends TestCase {
+ private static final String TAG = "SmartDialNameMatcherTest";
+
+ public void testMatches() {
+ // Test to ensure that all alphabetic characters are covered
+ checkMatches("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
+ "22233344455566677778889999" + "22233344455566677778889999", true, 0, 26 * 2);
+ // Should fail because of a mistyped 2 instead of 9 in the second last character
+ checkMatches("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
+ "22233344455566677778889999" + "22233344455566677778889929", false, 0, 0);
+
+ // Basic name test
+ checkMatches("joe", "5", true, 0, 1);
+ checkMatches("joe", "56", true, 0, 2);
+ checkMatches("joe", "563", true, 0, 3);
+
+ // Matches only word boundary.
+ checkMatches("joe", "63", false, 0, 0);
+ checkMatches("joe oe", "63", true, 4, 6);
+
+ // Test for a match across word boundaries
+ checkMatches("joe oe", "56363", true, 0, 6);
+ }
+
+ public void testMatches_repeatedLetters() {
+ checkMatches("aaaaaaaaaa", "2222222222", true, 0, 10);
+ // Fails because of one extra 2
+ checkMatches("aaaaaaaaaa", "22222222222", false, 0, 0);
+ checkMatches("zzzzzzzzzz zzzzzzzzzz", "99999999999999999999", true, 0, 21);
+ }
+
+ public void testMatches_repeatedSpaces() {
+ checkMatches("William J Smith", "9455426576", true, 0, 17);
+ checkMatches("William J Smith", "576", true, 12, 17);
+ // Fails because we start at non-word boundary
+ checkMatches("William J Smith", "6576", false, 0, 0);
+ }
+
+ // TODO: Do we want to make these pass anymore?
+ @Suppress
+ public void testMatches_repeatedSeparators() {
+ // Simple match for single token
+ checkMatches("John,,,,,Doe", "5646", true, 0, 4);
+ // Match across tokens
+ checkMatches("John,,,,,Doe", "56463", true, 0, 10);
+ // Match token after chain of separators
+ checkMatches("John,,,,,Doe", "363", true, 9, 12);
+ }
+
+ public void testMatches_umlaut() {
+ checkMatches("ÄÖÜäöü", "268268", true, 0, 6);
+ }
+ // TODO: Great if it was treated as "s" or "ss. Figure out if possible without prefix trie?
+ @Suppress
+ public void testMatches_germanSharpS() {
+ checkMatches("ß", "s", true, 0, 1);
+ checkMatches("ß", "ss", true, 0, 1);
+ }
+
+ // TODO: Add this and make it work
+ @Suppress
+ public void testMatches_greek() {
+ // http://en.wikipedia.org/wiki/Greek_alphabet
+ fail("Greek letters aren't supported yet.");
+ }
+
+ // TODO: Add this and make it work
+ @Suppress
+ public void testMatches_cyrillic() {
+ // http://en.wikipedia.org/wiki/Cyrillic_script
+ fail("Cyrillic letters aren't supported yet.");
+ }
+
+ private void checkMatches(String displayName, String query, boolean expectedMatches,
+ int expectedMatchStart, int expectedMatchEnd) {
+ final SmartDialNameMatcher matcher = new SmartDialNameMatcher(query);
+ final ArrayList<SmartDialMatchPosition> matchPositions =
+ new ArrayList<SmartDialMatchPosition>();
+ final boolean matches = matcher.matchesCombination(
+ displayName, query, matchPositions);
+ Log.d(TAG, "query=" + query + " text=" + displayName
+ + " nfd=" + Normalizer.normalize(displayName, Normalizer.Form.NFD)
+ + " nfc=" + Normalizer.normalize(displayName, Normalizer.Form.NFC)
+ + " nfkd=" + Normalizer.normalize(displayName, Normalizer.Form.NFKD)
+ + " nfkc=" + Normalizer.normalize(displayName, Normalizer.Form.NFKC)
+ + " matches=" + matches);
+ assertEquals("matches", expectedMatches, matches);
+ if (matches) {
+ assertEquals("start", expectedMatchStart, matchPositions.get(0).start);
+ assertEquals("end", expectedMatchEnd, matchPositions.get(0).end);
+ }
+ }
+}