aboutsummaryrefslogtreecommitdiffstats
path: root/java/src/com/android/inputmethod/latin/ContactsManager.java
diff options
context:
space:
mode:
authorTom Ouyang <ouyang@google.com>2015-04-13 17:49:17 -0700
committerTom Ouyang <ouyang@google.com>2015-04-16 11:33:54 -0700
commit44a175732dc4b872515f978b986ef7b357fe2f00 (patch)
treebe4c2e92514d7f5ac8306149ba469607ffb0f0e7 /java/src/com/android/inputmethod/latin/ContactsManager.java
parent40f0f61bb365b5073f1d9fdb56a393c5df5ef4b0 (diff)
downloadlatinime-44a175732dc4b872515f978b986ef7b357fe2f00.tar.gz
latinime-44a175732dc4b872515f978b986ef7b357fe2f00.tar.xz
latinime-44a175732dc4b872515f978b986ef7b357fe2f00.zip
Add affinity model for contact names.
This allows us to: 1. Rank contacts and only add the top N names to the keyboard LM. 2. Avoid adding duplicate names. Note: The affinity calcualuation is limited by the fact that some apps currently do not update the TIMES_CONTACTED counter. To better handle this case, the new measure also takes into account whether or not a name is in the visible contacts group. Bug: 20053274 Change-Id: I2741cb8958667d4a294aba8c437a45cec4b42dc7
Diffstat (limited to 'java/src/com/android/inputmethod/latin/ContactsManager.java')
-rw-r--r--java/src/com/android/inputmethod/latin/ContactsManager.java99
1 files changed, 93 insertions, 6 deletions
diff --git a/java/src/com/android/inputmethod/latin/ContactsManager.java b/java/src/com/android/inputmethod/latin/ContactsManager.java
index 7a971cf75..13503ff45 100644
--- a/java/src/com/android/inputmethod/latin/ContactsManager.java
+++ b/java/src/com/android/inputmethod/latin/ContactsManager.java
@@ -21,11 +21,17 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.net.Uri;
import android.provider.ContactsContract.Contacts;
+import android.text.TextUtils;
import android.util.Log;
import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.StringUtils;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -38,6 +44,63 @@ public class ContactsManager {
private static final String TAG = "ContactsManager";
/**
+ * Use at most this many of the highest affinity contacts.
+ */
+ public static final int MAX_CONTACT_NAMES = 200;
+
+ protected static class RankedContact {
+ public final String mName;
+ public final long mLastContactedTime;
+ public final int mTimesContacted;
+ public final boolean mInVisibleGroup;
+
+ private float mAffinity = 0.0f;
+
+ RankedContact(final Cursor cursor) {
+ mName = cursor.getString(
+ ContactsDictionaryConstants.NAME_INDEX);
+ mTimesContacted = cursor.getInt(
+ ContactsDictionaryConstants.TIMES_CONTACTED_INDEX);
+ mLastContactedTime = cursor.getLong(
+ ContactsDictionaryConstants.LAST_TIME_CONTACTED_INDEX);
+ mInVisibleGroup = cursor.getInt(
+ ContactsDictionaryConstants.IN_VISIBLE_GROUP_INDEX) == 1;
+ }
+
+ float getAffinity() {
+ return mAffinity;
+ }
+
+ /**
+ * Calculates the affinity with the contact based on:
+ * - How many times it has been contacted
+ * - How long since the last contact.
+ * - Whether the contact is in the visible group (i.e., Contacts list).
+ *
+ * Note: This affinity is limited by the fact that some apps currently do not update the
+ * LAST_TIME_CONTACTED or TIMES_CONTACTED counters. As a result, a frequently messaged
+ * contact may still have 0 affinity.
+ */
+ void computeAffinity(final int maxTimesContacted, final long currentTime) {
+ final float timesWeight = ((float) mTimesContacted + 1) / (maxTimesContacted + 1);
+ final long timeSinceLastContact = Math.min(
+ Math.max(0, currentTime - mLastContactedTime),
+ TimeUnit.MILLISECONDS.convert(180, TimeUnit.DAYS));
+ final float lastTimeWeight = (float) Math.pow(0.5,
+ timeSinceLastContact / (TimeUnit.MILLISECONDS.convert(10, TimeUnit.DAYS)));
+ final float visibleWeight = mInVisibleGroup ? 1.0f : 0.0f;
+ mAffinity = (timesWeight + lastTimeWeight + visibleWeight) / 3;
+ }
+ }
+
+ private static class AffinityComparator implements Comparator<RankedContact> {
+ @Override
+ public int compare(RankedContact contact1, RankedContact contact2) {
+ return Float.compare(contact2.getAffinity(), contact1.getAffinity());
+ }
+ }
+
+ /**
* Interface to implement for classes interested in getting notified for updates
* to Contacts content provider.
*/
@@ -82,14 +145,18 @@ public class ContactsManager {
* Returns all the valid names in the Contacts DB. Callers should also
* call {@link #updateLocalState(ArrayList)} after they are done with result
* so that the manager can cache local state for determining updates.
+ *
+ * These names are sorted by their affinity to the user, with favorite
+ * contacts appearing first.
*/
public ArrayList<String> getValidNames(final Uri uri) {
- final ArrayList<String> names = new ArrayList<>();
// Check all contacts since it's not possible to find out which names have changed.
// This is needed because it's possible to receive extraneous onChange events even when no
// name has changed.
final Cursor cursor = mContext.getContentResolver().query(uri,
ContactsDictionaryConstants.PROJECTION, null, null, null);
+ final ArrayList<RankedContact> contacts = new ArrayList<>();
+ int maxTimesContacted = 0;
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
@@ -97,7 +164,12 @@ public class ContactsManager {
final String name = cursor.getString(
ContactsDictionaryConstants.NAME_INDEX);
if (isValidName(name)) {
- names.add(name);
+ final int timesContacted = cursor.getInt(
+ ContactsDictionaryConstants.TIMES_CONTACTED_INDEX);
+ if (timesContacted > maxTimesContacted) {
+ maxTimesContacted = timesContacted;
+ }
+ contacts.add(new RankedContact(cursor));
}
cursor.moveToNext();
}
@@ -106,7 +178,16 @@ public class ContactsManager {
cursor.close();
}
}
- return names;
+ final long currentTime = System.currentTimeMillis();
+ for (RankedContact contact : contacts) {
+ contact.computeAffinity(maxTimesContacted, currentTime);
+ }
+ Collections.sort(contacts, new AffinityComparator());
+ final HashSet<String> names = new HashSet<>();
+ for (int i = 0; i < contacts.size() && names.size() < MAX_CONTACT_NAMES; ++i) {
+ names.add(contacts.get(i).mName);
+ }
+ return new ArrayList<>(names);
}
/**
@@ -134,10 +215,16 @@ public class ContactsManager {
}
private static boolean isValidName(final String name) {
- if (name != null && -1 == name.indexOf(Constants.CODE_COMMERCIAL_AT)) {
- return true;
+ if (TextUtils.isEmpty(name) || name.indexOf(Constants.CODE_COMMERCIAL_AT) != -1) {
+ return false;
+ }
+ final boolean hasSpace = name.indexOf(Constants.CODE_SPACE) != -1;
+ if (!hasSpace) {
+ // Only allow an isolated word if it does not contain a hyphen.
+ // This helps to filter out mailing lists.
+ return name.indexOf(Constants.CODE_DASH) == -1;
}
- return false;
+ return true;
}
/**