Home | History | Annotate | Download | only in latin
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.inputmethod.latin;
     18 
     19 import android.content.Context;
     20 import android.database.Cursor;
     21 import android.database.sqlite.SQLiteException;
     22 import android.net.Uri;
     23 import android.provider.ContactsContract.Contacts;
     24 import android.text.TextUtils;
     25 import android.util.Log;
     26 
     27 import com.android.inputmethod.latin.common.Constants;
     28 import com.android.inputmethod.latin.common.StringUtils;
     29 
     30 import java.util.ArrayList;
     31 import java.util.Collections;
     32 import java.util.Comparator;
     33 import java.util.HashSet;
     34 import java.util.concurrent.TimeUnit;
     35 import java.util.concurrent.atomic.AtomicInteger;
     36 
     37 /**
     38  * Manages all interactions with Contacts DB.
     39  *
     40  * The manager provides an API for listening to meaning full updates by keeping a
     41  * measure of the current state of the content provider.
     42  */
     43 public class ContactsManager {
     44     private static final String TAG = "ContactsManager";
     45 
     46     /**
     47      * Use at most this many of the highest affinity contacts.
     48      */
     49     public static final int MAX_CONTACT_NAMES = 200;
     50 
     51     protected static class RankedContact {
     52         public final String mName;
     53         public final long mLastContactedTime;
     54         public final int mTimesContacted;
     55         public final boolean mInVisibleGroup;
     56 
     57         private float mAffinity = 0.0f;
     58 
     59         RankedContact(final Cursor cursor) {
     60             mName = cursor.getString(
     61                     ContactsDictionaryConstants.NAME_INDEX);
     62             mTimesContacted = cursor.getInt(
     63                     ContactsDictionaryConstants.TIMES_CONTACTED_INDEX);
     64             mLastContactedTime = cursor.getLong(
     65                     ContactsDictionaryConstants.LAST_TIME_CONTACTED_INDEX);
     66             mInVisibleGroup = cursor.getInt(
     67                     ContactsDictionaryConstants.IN_VISIBLE_GROUP_INDEX) == 1;
     68         }
     69 
     70         float getAffinity() {
     71             return mAffinity;
     72         }
     73 
     74         /**
     75          * Calculates the affinity with the contact based on:
     76          * - How many times it has been contacted
     77          * - How long since the last contact.
     78          * - Whether the contact is in the visible group (i.e., Contacts list).
     79          *
     80          * Note: This affinity is limited by the fact that some apps currently do not update the
     81          * LAST_TIME_CONTACTED or TIMES_CONTACTED counters. As a result, a frequently messaged
     82          * contact may still have 0 affinity.
     83          */
     84         void computeAffinity(final int maxTimesContacted, final long currentTime) {
     85             final float timesWeight = ((float) mTimesContacted + 1) / (maxTimesContacted + 1);
     86             final long timeSinceLastContact = Math.min(
     87                     Math.max(0, currentTime - mLastContactedTime),
     88                     TimeUnit.MILLISECONDS.convert(180, TimeUnit.DAYS));
     89             final float lastTimeWeight = (float) Math.pow(0.5,
     90                     timeSinceLastContact / (TimeUnit.MILLISECONDS.convert(10, TimeUnit.DAYS)));
     91             final float visibleWeight = mInVisibleGroup ? 1.0f : 0.0f;
     92             mAffinity = (timesWeight + lastTimeWeight + visibleWeight) / 3;
     93         }
     94     }
     95 
     96     private static class AffinityComparator implements Comparator<RankedContact> {
     97         @Override
     98         public int compare(RankedContact contact1, RankedContact contact2) {
     99             return Float.compare(contact2.getAffinity(), contact1.getAffinity());
    100         }
    101     }
    102 
    103     /**
    104      * Interface to implement for classes interested in getting notified for updates
    105      * to Contacts content provider.
    106      */
    107     public static interface ContactsChangedListener {
    108         public void onContactsChange();
    109     }
    110 
    111     /**
    112      * The number of contacts observed in the most recent instance of
    113      * contacts content provider.
    114      */
    115     private AtomicInteger mContactCountAtLastRebuild = new AtomicInteger(0);
    116 
    117     /**
    118      * The hash code of list of valid contacts names in the most recent dictionary
    119      * rebuild.
    120      */
    121     private AtomicInteger mHashCodeAtLastRebuild = new AtomicInteger(0);
    122 
    123     private final Context mContext;
    124     private final ContactsContentObserver mObserver;
    125 
    126     public ContactsManager(final Context context) {
    127         mContext = context;
    128         mObserver = new ContactsContentObserver(this /* ContactsManager */, context);
    129     }
    130 
    131     // TODO: This was synchronized in previous version. Why?
    132     public void registerForUpdates(final ContactsChangedListener listener) {
    133         mObserver.registerObserver(listener);
    134     }
    135 
    136     public int getContactCountAtLastRebuild() {
    137         return mContactCountAtLastRebuild.get();
    138     }
    139 
    140     public int getHashCodeAtLastRebuild() {
    141         return mHashCodeAtLastRebuild.get();
    142     }
    143 
    144     /**
    145      * Returns all the valid names in the Contacts DB. Callers should also
    146      * call {@link #updateLocalState(ArrayList)} after they are done with result
    147      * so that the manager can cache local state for determining updates.
    148      *
    149      * These names are sorted by their affinity to the user, with favorite
    150      * contacts appearing first.
    151      */
    152     public ArrayList<String> getValidNames(final Uri uri) {
    153         // Check all contacts since it's not possible to find out which names have changed.
    154         // This is needed because it's possible to receive extraneous onChange events even when no
    155         // name has changed.
    156         final Cursor cursor = mContext.getContentResolver().query(uri,
    157                 ContactsDictionaryConstants.PROJECTION, null, null, null);
    158         final ArrayList<RankedContact> contacts = new ArrayList<>();
    159         int maxTimesContacted = 0;
    160         if (cursor != null) {
    161             try {
    162                 if (cursor.moveToFirst()) {
    163                     while (!cursor.isAfterLast()) {
    164                         final String name = cursor.getString(
    165                                 ContactsDictionaryConstants.NAME_INDEX);
    166                         if (isValidName(name)) {
    167                             final int timesContacted = cursor.getInt(
    168                                     ContactsDictionaryConstants.TIMES_CONTACTED_INDEX);
    169                             if (timesContacted > maxTimesContacted) {
    170                                 maxTimesContacted = timesContacted;
    171                             }
    172                             contacts.add(new RankedContact(cursor));
    173                         }
    174                         cursor.moveToNext();
    175                     }
    176                 }
    177             } finally {
    178                 cursor.close();
    179             }
    180         }
    181         final long currentTime = System.currentTimeMillis();
    182         for (RankedContact contact : contacts) {
    183             contact.computeAffinity(maxTimesContacted, currentTime);
    184         }
    185         Collections.sort(contacts, new AffinityComparator());
    186         final HashSet<String> names = new HashSet<>();
    187         for (int i = 0; i < contacts.size() && names.size() < MAX_CONTACT_NAMES; ++i) {
    188             names.add(contacts.get(i).mName);
    189         }
    190         return new ArrayList<>(names);
    191     }
    192 
    193     /**
    194      * Returns the number of contacts in contacts content provider.
    195      */
    196     public int getContactCount() {
    197         // TODO: consider switching to a rawQuery("select count(*)...") on the database if
    198         // performance is a bottleneck.
    199         Cursor cursor = null;
    200         try {
    201             cursor = mContext.getContentResolver().query(Contacts.CONTENT_URI,
    202                     ContactsDictionaryConstants.PROJECTION_ID_ONLY, null, null, null);
    203             if (null == cursor) {
    204                 return 0;
    205             }
    206             return cursor.getCount();
    207         } catch (final SQLiteException e) {
    208             Log.e(TAG, "SQLiteException in the remote Contacts process.", e);
    209         } finally {
    210             if (null != cursor) {
    211                 cursor.close();
    212             }
    213         }
    214         return 0;
    215     }
    216 
    217     private static boolean isValidName(final String name) {
    218         if (TextUtils.isEmpty(name) || name.indexOf(Constants.CODE_COMMERCIAL_AT) != -1) {
    219             return false;
    220         }
    221         final boolean hasSpace = name.indexOf(Constants.CODE_SPACE) != -1;
    222         if (!hasSpace) {
    223             // Only allow an isolated word if it does not contain a hyphen.
    224             // This helps to filter out mailing lists.
    225             return name.indexOf(Constants.CODE_DASH) == -1;
    226         }
    227         return true;
    228     }
    229 
    230     /**
    231      * Updates the local state of the manager. This should be called when the callers
    232      * are done with all the updates of the content provider successfully.
    233      */
    234     public void updateLocalState(final ArrayList<String> names) {
    235         mContactCountAtLastRebuild.set(getContactCount());
    236         mHashCodeAtLastRebuild.set(names.hashCode());
    237     }
    238 
    239     /**
    240      * Performs any necessary cleanup.
    241      */
    242     public void close() {
    243         mObserver.unregister();
    244     }
    245 }
    246