1/*
2 * Copyright (C) 2012 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
17package com.android.dialer.dialpad;
18
19import static com.android.dialer.dialpad.SmartDialController.LOG_TAG;
20
21import android.os.AsyncTask;
22import android.provider.ContactsContract;
23import android.provider.ContactsContract.Contacts;
24import android.telephony.PhoneNumberUtils;
25import android.util.Log;
26
27import com.android.contacts.common.preference.ContactsPreferences;
28import com.android.contacts.common.util.StopWatch;
29import com.android.dialer.dialpad.SmartDialCache.ContactNumber;
30
31import com.google.common.base.Objects;
32import com.google.common.collect.Lists;
33
34import java.util.ArrayList;
35import java.util.Collections;
36import java.util.HashSet;
37import java.util.List;
38import java.util.Set;
39
40/**
41 * This task searches through the provided cache to return the top 3 contacts(ranked by confidence)
42 * that match the query, then passes it back to the {@link SmartDialLoaderCallback} through a
43 * callback function.
44 */
45public class SmartDialLoaderTask extends AsyncTask<String, Integer, List<SmartDialEntry>> {
46
47    public interface SmartDialLoaderCallback {
48        void setSmartDialAdapterEntries(List<SmartDialEntry> list, String query);
49    }
50
51    static private final boolean DEBUG = false;
52
53    private static final int MAX_ENTRIES = 3;
54
55    private final SmartDialCache mContactsCache;
56
57    private final SmartDialLoaderCallback mCallback;
58
59    private final String mQuery;
60
61    /**
62     * See {@link ContactsPreferences#getDisplayOrder()}.
63     * {@link ContactsContract.Preferences#DISPLAY_ORDER_PRIMARY} (first name first)
64     * {@link ContactsContract.Preferences#DISPLAY_ORDER_ALTERNATIVE} (last name first)
65     */
66    private final SmartDialNameMatcher mNameMatcher;
67
68    public SmartDialLoaderTask(SmartDialLoaderCallback callback, String query,
69            SmartDialCache cache) {
70        this.mCallback = callback;
71        this.mNameMatcher = new SmartDialNameMatcher(PhoneNumberUtils.normalizeNumber(query));
72        this.mContactsCache = cache;
73        this.mQuery = query;
74    }
75
76    @Override
77    protected List<SmartDialEntry> doInBackground(String... params) {
78        return getContactMatches();
79    }
80
81    @Override
82    protected void onPostExecute(List<SmartDialEntry> result) {
83        if (mCallback != null) {
84            mCallback.setSmartDialAdapterEntries(result, mQuery);
85        }
86    }
87
88    /**
89     * Loads all visible contacts with phone numbers and check if their display names match the
90     * query.  Return at most {@link #MAX_ENTRIES} {@link SmartDialEntry}'s for the matching
91     * contacts.
92     */
93    private ArrayList<SmartDialEntry> getContactMatches() {
94
95        final SmartDialTrie trie = mContactsCache.getContacts();
96        final boolean matchNanp = mContactsCache.getUserInNanpRegion();
97
98        if (DEBUG) {
99            Log.d(LOG_TAG, "Size of cache: " + trie.size());
100        }
101
102        final StopWatch stopWatch = DEBUG ? StopWatch.start("Start Match") : null;
103        final ArrayList<ContactNumber> allMatches = trie.getAllWithPrefix(mNameMatcher.getQuery());
104        if (DEBUG) {
105            stopWatch.lap("Find matches");
106        }
107        // Sort matches in order of ascending contact affinity (lower is better)
108        Collections.sort(allMatches, new SmartDialCache.ContactAffinityComparator());
109        if (DEBUG) {
110            stopWatch.lap("Sort");
111        }
112        final Set<ContactMatch> duplicates = new HashSet<ContactMatch>();
113        final ArrayList<SmartDialEntry> candidates = Lists.newArrayList();
114        for (ContactNumber contact : allMatches) {
115            final ContactMatch contactMatch = new ContactMatch(contact.lookupKey, contact.id);
116            // Don't add multiple contact numbers from the same contact into suggestions if
117            // there are multiple matches. Instead, just keep the highest priority number
118            // instead.
119            if (duplicates.contains(contactMatch)) {
120                continue;
121            }
122            duplicates.add(contactMatch);
123            final boolean matches = mNameMatcher.matches(contact.displayName);
124
125            candidates.add(new SmartDialEntry(
126                    contact.displayName,
127                    Contacts.getLookupUri(contact.id, contact.lookupKey),
128                    contact.phoneNumber,
129                    mNameMatcher.getMatchPositions(),
130                    SmartDialNameMatcher.matchesNumber(contact.phoneNumber,
131                            mNameMatcher.getQuery(), matchNanp)
132                    ));
133            if (candidates.size() >= MAX_ENTRIES) {
134                break;
135            }
136        }
137        if (DEBUG) {
138            stopWatch.stopAndLog(LOG_TAG + " Match Complete", 0);
139        }
140        return candidates;
141    }
142
143    private class ContactMatch {
144        public final String lookupKey;
145        public final long id;
146
147        public ContactMatch(String lookupKey, long id) {
148            this.lookupKey = lookupKey;
149            this.id = id;
150        }
151
152        @Override
153        public int hashCode() {
154            return Objects.hashCode(lookupKey, id);
155        }
156
157        @Override
158        public boolean equals(Object object) {
159            if (this == object) {
160                return true;
161            }
162            if (object instanceof ContactMatch) {
163                ContactMatch that = (ContactMatch) object;
164                return Objects.equal(this.lookupKey, that.lookupKey)
165                        && Objects.equal(this.id, that.id);
166            }
167            return false;
168        }
169    }
170}
171