44a175732d
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
245 lines
9.2 KiB
Java
245 lines
9.2 KiB
Java
/*
|
|
* Copyright (C) 2014 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.inputmethod.latin;
|
|
|
|
import android.content.Context;
|
|
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;
|
|
|
|
/**
|
|
* Manages all interactions with Contacts DB.
|
|
*
|
|
* The manager provides an API for listening to meaning full updates by keeping a
|
|
* measure of the current state of the content provider.
|
|
*/
|
|
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.
|
|
*/
|
|
public static interface ContactsChangedListener {
|
|
public void onContactsChange();
|
|
}
|
|
|
|
/**
|
|
* The number of contacts observed in the most recent instance of
|
|
* contacts content provider.
|
|
*/
|
|
private AtomicInteger mContactCountAtLastRebuild = new AtomicInteger(0);
|
|
|
|
/**
|
|
* The hash code of list of valid contacts names in the most recent dictionary
|
|
* rebuild.
|
|
*/
|
|
private AtomicInteger mHashCodeAtLastRebuild = new AtomicInteger(0);
|
|
|
|
private final Context mContext;
|
|
private final ContactsContentObserver mObserver;
|
|
|
|
public ContactsManager(final Context context) {
|
|
mContext = context;
|
|
mObserver = new ContactsContentObserver(this /* ContactsManager */, context);
|
|
}
|
|
|
|
// TODO: This was synchronized in previous version. Why?
|
|
public void registerForUpdates(final ContactsChangedListener listener) {
|
|
mObserver.registerObserver(listener);
|
|
}
|
|
|
|
public int getContactCountAtLastRebuild() {
|
|
return mContactCountAtLastRebuild.get();
|
|
}
|
|
|
|
public int getHashCodeAtLastRebuild() {
|
|
return mHashCodeAtLastRebuild.get();
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
// 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()) {
|
|
while (!cursor.isAfterLast()) {
|
|
final String name = cursor.getString(
|
|
ContactsDictionaryConstants.NAME_INDEX);
|
|
if (isValidName(name)) {
|
|
final int timesContacted = cursor.getInt(
|
|
ContactsDictionaryConstants.TIMES_CONTACTED_INDEX);
|
|
if (timesContacted > maxTimesContacted) {
|
|
maxTimesContacted = timesContacted;
|
|
}
|
|
contacts.add(new RankedContact(cursor));
|
|
}
|
|
cursor.moveToNext();
|
|
}
|
|
}
|
|
} finally {
|
|
cursor.close();
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Returns the number of contacts in contacts content provider.
|
|
*/
|
|
public int getContactCount() {
|
|
// TODO: consider switching to a rawQuery("select count(*)...") on the database if
|
|
// performance is a bottleneck.
|
|
Cursor cursor = null;
|
|
try {
|
|
cursor = mContext.getContentResolver().query(Contacts.CONTENT_URI,
|
|
ContactsDictionaryConstants.PROJECTION_ID_ONLY, null, null, null);
|
|
if (null == cursor) {
|
|
return 0;
|
|
}
|
|
return cursor.getCount();
|
|
} catch (final SQLiteException e) {
|
|
Log.e(TAG, "SQLiteException in the remote Contacts process.", e);
|
|
} finally {
|
|
if (null != cursor) {
|
|
cursor.close();
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
private static boolean isValidName(final String name) {
|
|
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
|
|
* are done with all the updates of the content provider successfully.
|
|
*/
|
|
public void updateLocalState(final ArrayList<String> names) {
|
|
mContactCountAtLastRebuild.set(getContactCount());
|
|
mHashCodeAtLastRebuild.set(names.hashCode());
|
|
}
|
|
|
|
/**
|
|
* Performs any necessary cleanup.
|
|
*/
|
|
public void close() {
|
|
mObserver.unregister();
|
|
}
|
|
}
|