aboutsummaryrefslogtreecommitdiffstats
path: root/java/src/org/kelar/inputmethod/latin/ContactsManager.java
diff options
context:
space:
mode:
authorAmin Bandali <bandali@kelar.org>2024-12-16 21:45:41 -0500
committerAmin Bandali <bandali@kelar.org>2025-01-11 14:17:35 -0500
commite9a0e66716dab4dd3184d009d8920de1961efdfa (patch)
tree02dcc096643d74645bf28459c2834c3d4a2ad7f2 /java/src/org/kelar/inputmethod/latin/ContactsManager.java
parentfb3b9360d70596d7e921de8bf7d3ca99564a077e (diff)
downloadlatinime-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.java244
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();
+ }
+}