diff options
author | 2024-12-16 21:45:41 -0500 | |
---|---|---|
committer | 2025-01-11 14:17:35 -0500 | |
commit | e9a0e66716dab4dd3184d009d8920de1961efdfa (patch) | |
tree | 02dcc096643d74645bf28459c2834c3d4a2ad7f2 /java/src/org/kelar/inputmethod/latin/ContactsManager.java | |
parent | fb3b9360d70596d7e921de8bf7d3ca99564a077e (diff) | |
download | latinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.tar.gz latinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.tar.xz latinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.zip |
Rename to Kelar Keyboard (org.kelar.inputmethod.latin)
Diffstat (limited to 'java/src/org/kelar/inputmethod/latin/ContactsManager.java')
-rw-r--r-- | java/src/org/kelar/inputmethod/latin/ContactsManager.java | 244 |
1 files changed, 244 insertions, 0 deletions
diff --git a/java/src/org/kelar/inputmethod/latin/ContactsManager.java b/java/src/org/kelar/inputmethod/latin/ContactsManager.java new file mode 100644 index 000000000..e4a6912db --- /dev/null +++ b/java/src/org/kelar/inputmethod/latin/ContactsManager.java @@ -0,0 +1,244 @@ +/* + * 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 org.kelar.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 org.kelar.inputmethod.latin.common.Constants; + +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(); + } +} |