1/*
2 * Copyright (C) 2009 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.providers.contacts.aggregation.util;
18
19import android.database.Cursor;
20import android.database.sqlite.SQLiteDatabase;
21
22import com.android.providers.contacts.ContactsDatabaseHelper.NicknameLookupColumns;
23import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
24import com.google.android.collect.Maps;
25
26import java.lang.ref.SoftReference;
27import java.util.BitSet;
28import java.util.HashMap;
29
30/**
31 * Cache for common nicknames.
32 */
33public class CommonNicknameCache  {
34
35    // We will use this much memory (in bits) to optimize the nickname cluster lookup
36    private static final int NICKNAME_BLOOM_FILTER_SIZE = 0x1FFF;   // =long[128]
37    private BitSet mNicknameBloomFilter;
38
39    private HashMap<String, SoftReference<String[]>> mNicknameClusterCache = Maps.newHashMap();
40
41    private final SQLiteDatabase mDb;
42
43    public CommonNicknameCache(SQLiteDatabase db) {
44        mDb = db;
45    }
46
47    private final static class NicknameLookupPreloadQuery {
48        public final static String TABLE = Tables.NICKNAME_LOOKUP;
49
50        public final static String[] COLUMNS = new String[] {
51            NicknameLookupColumns.NAME
52        };
53
54        public final static int NAME = 0;
55    }
56
57    /**
58     * Read all known common nicknames from the database and populate a Bloom
59     * filter using the corresponding hash codes. The idea is to eliminate most
60     * of unnecessary database lookups for nicknames. Given a name, we will take
61     * its hash code and see if it is set in the Bloom filter. If not, we will know
62     * that the name is not in the database. If it is, we still need to run a
63     * query.
64     * <p>
65     * Given the size of the filter and the expected size of the nickname table,
66     * we should expect the combination of the Bloom filter and cache will
67     * prevent around 98-99% of unnecessary queries from running.
68     */
69    private void preloadNicknameBloomFilter() {
70        mNicknameBloomFilter = new BitSet(NICKNAME_BLOOM_FILTER_SIZE + 1);
71        Cursor cursor = mDb.query(NicknameLookupPreloadQuery.TABLE,
72                NicknameLookupPreloadQuery.COLUMNS,
73                null, null, null, null, null);
74        try {
75            int count = cursor.getCount();
76            for (int i = 0; i < count; i++) {
77                cursor.moveToNext();
78                String normalizedName = cursor.getString(NicknameLookupPreloadQuery.NAME);
79                int hashCode = normalizedName.hashCode();
80                mNicknameBloomFilter.set(hashCode & NICKNAME_BLOOM_FILTER_SIZE);
81            }
82        } finally {
83            cursor.close();
84        }
85    }
86
87    /**
88     * Returns nickname cluster IDs or null. Maintains cache.
89     */
90    public String[] getCommonNicknameClusters(String normalizedName) {
91        if (mNicknameBloomFilter == null) {
92            preloadNicknameBloomFilter();
93        }
94
95        int hashCode = normalizedName.hashCode();
96        if (!mNicknameBloomFilter.get(hashCode & NICKNAME_BLOOM_FILTER_SIZE)) {
97            return null;
98        }
99
100        SoftReference<String[]> ref;
101        String[] clusters = null;
102        synchronized (mNicknameClusterCache) {
103            if (mNicknameClusterCache.containsKey(normalizedName)) {
104                ref = mNicknameClusterCache.get(normalizedName);
105                if (ref == null) {
106                    return null;
107                }
108                clusters = ref.get();
109            }
110        }
111
112        if (clusters == null) {
113            clusters = loadNicknameClusters(normalizedName);
114            ref = clusters == null ? null : new SoftReference<String[]>(clusters);
115            synchronized (mNicknameClusterCache) {
116                mNicknameClusterCache.put(normalizedName, ref);
117            }
118        }
119        return clusters;
120    }
121
122    private interface NicknameLookupQuery {
123        String TABLE = Tables.NICKNAME_LOOKUP;
124
125        String[] COLUMNS = new String[] {
126            NicknameLookupColumns.CLUSTER
127        };
128
129        int CLUSTER = 0;
130    }
131
132    protected String[] loadNicknameClusters(String normalizedName) {
133        String[] clusters = null;
134        Cursor cursor = mDb.query(NicknameLookupQuery.TABLE, NicknameLookupQuery.COLUMNS,
135                NicknameLookupColumns.NAME + "=?", new String[] { normalizedName },
136                null, null, null);
137        try {
138            int count = cursor.getCount();
139            if (count > 0) {
140                clusters = new String[count];
141                for (int i = 0; i < count; i++) {
142                    cursor.moveToNext();
143                    clusters[i] = cursor.getString(NicknameLookupQuery.CLUSTER);
144                }
145            }
146        } finally {
147            cursor.close();
148        }
149        return clusters;
150    }
151}
152