| /* |
| * 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(); |
| } |
| } |