summaryrefslogtreecommitdiff
path: root/src/com/android/dialer/dialpad/SmartDialLoaderTask.java
blob: b4613b30d6b9a762b06e99b971753b5bd7b74feb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
/*
 * Copyright (C) 2012 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.dialer.dialpad;

import static com.android.dialer.dialpad.SmartDialAdapter.LOG_TAG;

import com.google.common.collect.Lists;

import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
import android.telephony.PhoneNumberUtils;
import android.util.Log;

import com.android.contacts.common.preference.ContactsPreferences;
import com.android.contacts.common.util.StopWatch;

import java.util.ArrayList;
import java.util.List;

/**
 * AsyncTask that performs one of two functions depending on which constructor is used.
 * If {@link #SmartDialLoaderTask(Context context, int nameDisplayOrder)} is used, the task
 * caches all contacts with a phone number into the static variable {@link #sContactsCache}.
 * If {@link #SmartDialLoaderTask(SmartDialLoaderCallback callback, String query)} is used, the
 * task searches through the cache to return the top 3 contacts(ranked by confidence) that match
 * the query, then passes it back to the {@link SmartDialLoaderCallback} through a callback
 * function.
 */
// TODO: Make the cache a singleton class and refactor to fix possible concurrency issues in the
// future
public class SmartDialLoaderTask extends AsyncTask<String, Integer, List<SmartDialEntry>> {

    private class Contact {
        final String mDisplayName;
        final String mStrippedDisplayName;
        final String mLookupKey;
        final long mId;

        public Contact(long id, String displayName, String lookupKey) {
            mDisplayName = displayName;
            mStrippedDisplayName = SmartDialNameMatcher.stripDiacritics(displayName);
            mLookupKey = lookupKey;
            mId = id;
        }
    }

    public interface SmartDialLoaderCallback {
        void setSmartDialAdapterEntries(List<SmartDialEntry> list);
    }

    static private final boolean DEBUG = true; // STOPSHIP change to false.

    private static final int MAX_ENTRIES = 3;

    private static List<Contact> sContactsCache;

    private final boolean mCacheOnly;

    private final SmartDialLoaderCallback mCallback;

    private final Context mContext;
    /**
     * See {@link ContactsPreferences#getDisplayOrder()}.
     * {@link ContactsContract.Preferences#DISPLAY_ORDER_PRIMARY} (first name first)
     * {@link ContactsContract.Preferences#DISPLAY_ORDER_ALTERNATIVE} (last name first)
     */
    private final int mNameDisplayOrder;

    private final SmartDialNameMatcher mNameMatcher;

    // cache only constructor
    private SmartDialLoaderTask(Context context, int nameDisplayOrder) {
        this.mNameDisplayOrder = nameDisplayOrder;
        this.mContext = context;
        // we're just caching contacts so no need to initialize a SmartDialNameMatcher or callback
        this.mNameMatcher = null;
        this.mCallback = null;
        this.mCacheOnly = true;
    }

    public SmartDialLoaderTask(SmartDialLoaderCallback callback, String query) {
        this.mCallback = callback;
        this.mContext = null;
        this.mCacheOnly = false;
        this.mNameDisplayOrder = 0;
        this.mNameMatcher = new SmartDialNameMatcher(PhoneNumberUtils.normalizeNumber(query));
    }

    @Override
    protected List<SmartDialEntry> doInBackground(String... params) {
        if (mCacheOnly) {
            cacheContacts();
            return Lists.newArrayList();
        }

        return getContactMatches();
    }

    @Override
    protected void onPostExecute(List<SmartDialEntry> result) {
        if (mCallback != null) {
            mCallback.setSmartDialAdapterEntries(result);
        }
    }

    /** Query used for loadByContactName */
    private interface ContactQuery {
        Uri URI = Contacts.CONTENT_URI.buildUpon()
                // Visible contact only
                //.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, "0")
                .build();
        String[] PROJECTION = new String[] {
                Contacts._ID,
                Contacts.DISPLAY_NAME,
                Contacts.LOOKUP_KEY
            };
        String[] PROJECTION_ALTERNATIVE = new String[] {
                Contacts._ID,
                Contacts.DISPLAY_NAME_ALTERNATIVE,
                Contacts.LOOKUP_KEY
            };

        int COLUMN_ID = 0;
        int COLUMN_DISPLAY_NAME = 1;
        int COLUMN_LOOKUP_KEY = 2;

        String SELECTION =
                //Contacts.IN_VISIBLE_GROUP + "=1 and " +
                Contacts.HAS_PHONE_NUMBER + "=1";

        String ORDER_BY = Contacts.LAST_TIME_CONTACTED + " DESC";
    }

    public static void startCacheContactsTaskIfNeeded(Context context, int displayOrder) {
        if (sContactsCache != null) {
            // contacts have already been cached, just return
            return;
        }
        final SmartDialLoaderTask task =
                new SmartDialLoaderTask(context, displayOrder);
        task.execute();
    }

    /**
     * Caches the contacts into an in memory array list. This is called once at startup and should
     * not be cancelled.
     */
    private void cacheContacts() {
        final StopWatch stopWatch = DEBUG ? StopWatch.start("SmartDial Cache") : null;
        if (sContactsCache != null) {
            // contacts have already been cached, just return
            stopWatch.stopAndLog("SmartDial Already Cached", 0);
            return;
        }
        if (mContext == null) {
            if (DEBUG) {
                stopWatch.stopAndLog("Invalid context", 0);
            }
            return;
        }
        final Cursor c = mContext.getContentResolver().query(ContactQuery.URI,
                (mNameDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY)
                    ? ContactQuery.PROJECTION : ContactQuery.PROJECTION_ALTERNATIVE,
                ContactQuery.SELECTION, null,
                ContactQuery.ORDER_BY);
        if (c == null) {
            if (DEBUG) {
                stopWatch.stopAndLog("Query Failure", 0);
            }
            return;
        }
        sContactsCache = Lists.newArrayListWithCapacity(c.getCount());
        try {
            c.moveToPosition(-1);
            while (c.moveToNext()) {
                final String displayName = c.getString(ContactQuery.COLUMN_DISPLAY_NAME);
                final long id = c.getLong(ContactQuery.COLUMN_ID);
                final String lookupKey = c.getString(ContactQuery.COLUMN_LOOKUP_KEY);
                sContactsCache.add(new Contact(id, displayName, lookupKey));
            }
        } finally {
            c.close();
            if (DEBUG) {
                stopWatch.stopAndLog("SmartDial Cache", 0);
            }
        }
    }

    /**
     * Loads all visible contacts with phone numbers and check if their display names match the
     * query.  Return at most {@link #MAX_ENTRIES} {@link SmartDialEntry}'s for the matching
     * contacts.
     */
    private ArrayList<SmartDialEntry> getContactMatches() {
        final StopWatch stopWatch = DEBUG ? StopWatch.start(LOG_TAG + " Start Match") : null;
        if (sContactsCache == null) {
            // contacts should have been cached by this point in time, but in case they
            // are not, we go ahead and cache them into memory.
            if (DEBUG) {
                Log.d(LOG_TAG, "empty cache");
            }
            cacheContacts();
            // TODO: if sContactsCache is still null at this point we should try to recache
        }
        if (DEBUG) {
            Log.d(LOG_TAG, "Size of cache: " + sContactsCache.size());
        }
        final ArrayList<SmartDialEntry> outList = Lists.newArrayList();
        if (sContactsCache == null) {
            return outList;
        }
        int count = 0;
        for (int i = 0; i < sContactsCache.size(); i++) {
            final Contact contact = sContactsCache.get(i);
            final String strippedDisplayName = contact.mStrippedDisplayName;

            if (!mNameMatcher.matches(strippedDisplayName)) {
                continue;
            }
            // Matched; create SmartDialEntry.
            @SuppressWarnings("unchecked")
            final SmartDialEntry entry = new SmartDialEntry(
                     contact.mDisplayName,
                     Contacts.getLookupUri(contact.mId, contact.mLookupKey),
                     (ArrayList<SmartDialMatchPosition>) mNameMatcher.getMatchPositions().clone()
                     );
            outList.add(entry);
            count++;
            if (count >= MAX_ENTRIES) {
                break;
            }
        }
        if (DEBUG) {
            stopWatch.stopAndLog(LOG_TAG + " Match Complete", 0);
        }
        return outList;
    }
}