Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit cdfe8b93 authored by Tom Ouyang's avatar Tom Ouyang Committed by Android (Google) Code Review
Browse files

Merge "Add affinity model for contact names."

parents c155f7f5 44a17573
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -84,10 +84,10 @@ public class ContactsContentObserver implements Runnable {
    boolean haveContentsChanged() {
        final long startTime = SystemClock.uptimeMillis();
        final int contactCount = mManager.getContactCount();
        if (contactCount > ContactsDictionaryConstants.MAX_CONTACT_COUNT) {
        if (contactCount > ContactsDictionaryConstants.MAX_CONTACTS_PROVIDER_QUERY_LIMIT) {
            // If there are too many contacts then return false. In this rare case it is impossible
            // to include all of them anyways and the cost of rebuilding the dictionary is too high.
            // TODO: Sort and check only the MAX_CONTACT_COUNT most recent contacts?
            // TODO: Sort and check only the most recent contacts?
            return false;
        }
        if (contactCount != mManager.getContactCountAtLastRebuild()) {
+7 −3
Original line number Diff line number Diff line
@@ -26,7 +26,8 @@ public class ContactsDictionaryConstants {
    /**
     * Projections for {@link Contacts.CONTENT_URI}
     */
    public static final String[] PROJECTION = { BaseColumns._ID, Contacts.DISPLAY_NAME };
    public static final String[] PROJECTION = { BaseColumns._ID, Contacts.DISPLAY_NAME,
            Contacts.TIMES_CONTACTED, Contacts.LAST_TIME_CONTACTED, Contacts.IN_VISIBLE_GROUP };
    public static final String[] PROJECTION_ID_ONLY = { BaseColumns._ID };

    /**
@@ -36,13 +37,16 @@ public class ContactsDictionaryConstants {
    public static final int FREQUENCY_FOR_CONTACTS_BIGRAM = 90;

    /**
     *  The maximum number of contacts that this dictionary supports.
     *  Do not attempt to query contacts if there are more than this many entries.
     */
    public static final int MAX_CONTACT_COUNT = 10000;
    public static final int MAX_CONTACTS_PROVIDER_QUERY_LIMIT = 10000;

    /**
     * Index of the column for 'name' in content providers:
     * Contacts & ContactsContract.Profile.
     */
    public static final int NAME_INDEX = 1;
    public static final int TIMES_CONTACTED_INDEX = 2;
    public static final int LAST_TIME_CONTACTED_INDEX = 3;
    public static final int IN_VISIBLE_GROUP_INDEX = 4;
}
+93 −6
Original line number Diff line number Diff line
@@ -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;

/**
@@ -37,6 +43,63 @@ import java.util.concurrent.atomic.AtomicInteger;
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,11 +215,17 @@ 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 true;
    }

    /**
     * Updates the local state of the manager. This should be called when the callers
+50 −10
Original line number Diff line number Diff line
@@ -29,11 +29,15 @@ import android.test.mock.MockContentProvider;
import android.test.mock.MockContentResolver;
import android.test.suitebuilder.annotation.SmallTest;

import com.android.inputmethod.latin.ContactsDictionaryConstants;
import com.android.inputmethod.latin.ContactsManager;

import org.junit.Before;
import org.junit.Test;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;

/**
 * Tests for {@link ContactsManager}
@@ -63,12 +67,13 @@ public class ContactsManagerTest extends AndroidTestCase {

    @Test
    public void testGetValidNames() {
        final String contactName1 = "firstname lastname";
        final String contactName1 = "firstname last-name";
        final String contactName2 = "larry";
        mMatrixCursor.addRow(new Object[] { 1, contactName1 });
        mMatrixCursor.addRow(new Object[] { 2, null /* null name */ });
        mMatrixCursor.addRow(new Object[] { 3, contactName2 });
        mMatrixCursor.addRow(new Object[] { 4, "floopy@example.com" /* invalid name */ });
        mMatrixCursor.addRow(new Object[] { 1, contactName1, 0, 0, 0 });
        mMatrixCursor.addRow(new Object[] { 2, null /* null name */, 0, 0, 0 });
        mMatrixCursor.addRow(new Object[] { 3, contactName2, 0, 0, 0 });
        mMatrixCursor.addRow(new Object[] { 4, "floopy@example.com" /* invalid name */, 0, 0, 0 });
        mMatrixCursor.addRow(new Object[] { 5, "news-group" /* invalid name */, 0, 0, 0 });
        mFakeContactsContentProvider.addQueryResult(Contacts.CONTENT_URI, mMatrixCursor);

        final ArrayList<String> validNames = mManager.getValidNames(Contacts.CONTENT_URI);
@@ -78,13 +83,48 @@ public class ContactsManagerTest extends AndroidTestCase {
    }

    @Test
    public void testGetCount() {
        mMatrixCursor.addRow(new Object[] { 1, "firstname" });
        mMatrixCursor.addRow(new Object[] { 2, null /* null name */ });
        mMatrixCursor.addRow(new Object[] { 3, "larry" });
        mMatrixCursor.addRow(new Object[] { 4, "floopy@example.com" /* invalid name */ });
    public void testGetValidNamesAffinity() {
        final long now = System.currentTimeMillis();
        final long month_ago = now - TimeUnit.MILLISECONDS.convert(31, TimeUnit.DAYS);
        for (int i = 0; i < ContactsManager.MAX_CONTACT_NAMES + 10; ++i) {
            mMatrixCursor.addRow(new Object[] { i, "name" + i, i, now, 1 });
        }
        mFakeContactsContentProvider.addQueryResult(Contacts.CONTENT_URI, mMatrixCursor);

        final ArrayList<String> validNames = mManager.getValidNames(Contacts.CONTENT_URI);
        assertEquals(ContactsManager.MAX_CONTACT_NAMES, validNames.size());
        for (int i = 0; i < 10; ++i) {
            assertFalse(validNames.contains("name" + i));
        }
        for (int i = 10; i < ContactsManager.MAX_CONTACT_NAMES + 10; ++i) {
            assertTrue(validNames.contains("name" + i));
        }
    }

    @Test
    public void testComputeAffinity() {
        final long now = System.currentTimeMillis();
        final long month_ago = now - TimeUnit.MILLISECONDS.convert(31, TimeUnit.DAYS);
        mMatrixCursor.addRow(new Object[] { 1, "name", 1, month_ago, 1 });
        mFakeContactsContentProvider.addQueryResult(Contacts.CONTENT_URI, mMatrixCursor);

        Cursor cursor = mFakeContactsContentProvider.query(Contacts.CONTENT_URI,
                ContactsDictionaryConstants.PROJECTION_ID_ONLY, null, null, null);
        cursor.moveToFirst();
        ContactsManager.RankedContact contact = new ContactsManager.RankedContact(cursor);
        contact.computeAffinity(1, month_ago);
        assertEquals(contact.getAffinity(), 1.0f);
        contact.computeAffinity(2, now);
        assertEquals(contact.getAffinity(), (2.0f/3.0f + (float)Math.pow(0.5, 3) + 1.0f) / 3);
    }

    @Test
    public void testGetCount() {
        mMatrixCursor.addRow(new Object[] { 1, "firstname", 0, 0, 0 });
        mMatrixCursor.addRow(new Object[] { 2, null /* null name */, 0, 0, 0 });
        mMatrixCursor.addRow(new Object[] { 3, "larry", 0, 0, 0 });
        mMatrixCursor.addRow(new Object[] { 4, "floopy@example.com" /* invalid name */, 0, 0, 0 });
        mFakeContactsContentProvider.addQueryResult(Contacts.CONTENT_URI, mMatrixCursor);
        assertEquals(4, mManager.getContactCount());
    }