1/*
2 * Copyright (C) 2013 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.database;
18
19import android.content.ContentValues;
20import android.content.Context;
21import android.content.SharedPreferences;
22import android.database.Cursor;
23import android.database.DatabaseUtils;
24import android.database.sqlite.SQLiteDatabase;
25import android.database.sqlite.SQLiteException;
26import android.database.sqlite.SQLiteOpenHelper;
27import android.database.sqlite.SQLiteStatement;
28import android.net.Uri;
29import android.os.AsyncTask;
30import android.provider.BaseColumns;
31import android.provider.ContactsContract;
32import android.provider.ContactsContract.CommonDataKinds.Phone;
33import android.provider.ContactsContract.Contacts;
34import android.provider.ContactsContract.Data;
35import android.provider.ContactsContract.Directory;
36import android.text.TextUtils;
37import android.util.Log;
38
39import com.android.contacts.common.util.StopWatch;
40import com.android.dialer.R;
41import com.android.dialer.dialpad.SmartDialNameMatcher;
42import com.android.dialer.dialpad.SmartDialPrefix;
43
44import com.google.common.annotations.VisibleForTesting;
45import com.google.common.base.Objects;
46import com.google.common.base.Preconditions;
47import com.google.common.collect.Lists;
48
49import java.util.ArrayList;
50import java.util.HashSet;
51import java.util.Set;
52import java.util.concurrent.atomic.AtomicBoolean;
53
54/**
55 * Database helper for smart dial. Designed as a singleton to make sure there is
56 * only one access point to the database. Provides methods to maintain, update,
57 * and query the database.
58 */
59public class DialerDatabaseHelper extends SQLiteOpenHelper {
60    private static final String TAG = "DialerDatabaseHelper";
61    private static final boolean DEBUG = false;
62
63    private static DialerDatabaseHelper sSingleton = null;
64
65    private static final Object mLock = new Object();
66    private static final AtomicBoolean sInUpdate = new AtomicBoolean(false);
67    private final Context mContext;
68
69    /**
70     * SmartDial DB version ranges:
71     * <pre>
72     *   0-98   KeyLimePie
73     * </pre>
74     */
75    public static final int DATABASE_VERSION = 4;
76    public static final String DATABASE_NAME = "dialer.db";
77
78    /**
79     * Saves the last update time of smart dial databases to shared preferences.
80     */
81    private static final String DATABASE_LAST_CREATED_SHARED_PREF = "com.android.dialer";
82    private static final String LAST_UPDATED_MILLIS = "last_updated_millis";
83    private static final String DATABASE_VERSION_PROPERTY = "database_version";
84
85    private static final int MAX_ENTRIES = 20;
86
87    public interface Tables {
88        /** Saves the necessary smart dial information of all contacts. */
89        static final String SMARTDIAL_TABLE = "smartdial_table";
90        /** Saves all possible prefixes to refer to a contacts.*/
91        static final String PREFIX_TABLE = "prefix_table";
92        /** Database properties for internal use */
93        static final String PROPERTIES = "properties";
94    }
95
96    public interface SmartDialDbColumns {
97        static final String _ID = "id";
98        static final String DATA_ID = "data_id";
99        static final String NUMBER = "phone_number";
100        static final String CONTACT_ID = "contact_id";
101        static final String LOOKUP_KEY = "lookup_key";
102        static final String DISPLAY_NAME_PRIMARY = "display_name";
103        static final String PHOTO_ID = "photo_id";
104        static final String LAST_TIME_USED = "last_time_used";
105        static final String TIMES_USED = "times_used";
106        static final String STARRED = "starred";
107        static final String IS_SUPER_PRIMARY = "is_super_primary";
108        static final String IN_VISIBLE_GROUP = "in_visible_group";
109        static final String IS_PRIMARY = "is_primary";
110        static final String LAST_SMARTDIAL_UPDATE_TIME = "last_smartdial_update_time";
111    }
112
113    public static interface PrefixColumns extends BaseColumns {
114        static final String PREFIX = "prefix";
115        static final String CONTACT_ID = "contact_id";
116    }
117
118    public interface PropertiesColumns {
119        String PROPERTY_KEY = "property_key";
120        String PROPERTY_VALUE = "property_value";
121    }
122
123    /** Query options for querying the contact database.*/
124    public static interface PhoneQuery {
125       static final Uri URI = Phone.CONTENT_URI.buildUpon().
126               appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
127                       String.valueOf(Directory.DEFAULT)).
128               appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true").
129               build();
130
131       static final String[] PROJECTION = new String[] {
132            Phone._ID,                          // 0
133            Phone.TYPE,                         // 1
134            Phone.LABEL,                        // 2
135            Phone.NUMBER,                       // 3
136            Phone.CONTACT_ID,                   // 4
137            Phone.LOOKUP_KEY,                   // 5
138            Phone.DISPLAY_NAME_PRIMARY,         // 6
139            Phone.PHOTO_ID,                     // 7
140            Data.LAST_TIME_USED,                // 8
141            Data.TIMES_USED,                    // 9
142            Contacts.STARRED,                   // 10
143            Data.IS_SUPER_PRIMARY,              // 11
144            Contacts.IN_VISIBLE_GROUP,          // 12
145            Data.IS_PRIMARY,                    // 13
146        };
147
148        static final int PHONE_ID = 0;
149        static final int PHONE_TYPE = 1;
150        static final int PHONE_LABEL = 2;
151        static final int PHONE_NUMBER = 3;
152        static final int PHONE_CONTACT_ID = 4;
153        static final int PHONE_LOOKUP_KEY = 5;
154        static final int PHONE_DISPLAY_NAME = 6;
155        static final int PHONE_PHOTO_ID = 7;
156        static final int PHONE_LAST_TIME_USED = 8;
157        static final int PHONE_TIMES_USED = 9;
158        static final int PHONE_STARRED = 10;
159        static final int PHONE_IS_SUPER_PRIMARY = 11;
160        static final int PHONE_IN_VISIBLE_GROUP = 12;
161        static final int PHONE_IS_PRIMARY = 13;
162
163        /** Selects only rows that have been updated after a certain time stamp.*/
164        static final String SELECT_UPDATED_CLAUSE =
165                Phone.CONTACT_LAST_UPDATED_TIMESTAMP + " > ?";
166
167        /** Ignores contacts that have an unreasonably long lookup key. These are likely to be
168         * the result of multiple (> 50) merged raw contacts, and are likely to cause
169         * OutOfMemoryExceptions within SQLite, or cause memory allocation problems later on
170         * when iterating through the cursor set (see b/13133579)
171         */
172        static final String SELECT_IGNORE_LOOKUP_KEY_TOO_LONG_CLAUSE =
173                "length(" + Phone.LOOKUP_KEY + ") < 1000";
174
175        static final String SELECTION = SELECT_UPDATED_CLAUSE + " AND " +
176                SELECT_IGNORE_LOOKUP_KEY_TOO_LONG_CLAUSE;
177    }
178
179    /** Query options for querying the deleted contact database.*/
180    public static interface DeleteContactQuery {
181       static final Uri URI = ContactsContract.DeletedContacts.CONTENT_URI;
182
183       static final String[] PROJECTION = new String[] {
184            ContactsContract.DeletedContacts.CONTACT_ID,                          // 0
185            ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP,           // 1
186        };
187
188        static final int DELETED_CONTACT_ID = 0;
189        static final int DELECTED_TIMESTAMP = 1;
190
191        /** Selects only rows that have been deleted after a certain time stamp.*/
192        public static final String SELECT_UPDATED_CLAUSE =
193                ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP + " > ?";
194    }
195
196    /**
197     * Gets the sorting order for the smartdial table. This computes a SQL "ORDER BY" argument by
198     * composing contact status and recent contact details together.
199     */
200    private static interface SmartDialSortingOrder {
201        /** Current contacts - those contacted within the last 3 days (in milliseconds) */
202        static final long LAST_TIME_USED_CURRENT_MS = 3L * 24 * 60 * 60 * 1000;
203        /** Recent contacts - those contacted within the last 30 days (in milliseconds) */
204        static final long LAST_TIME_USED_RECENT_MS = 30L * 24 * 60 * 60 * 1000;
205
206        /** Time since last contact. */
207        static final String TIME_SINCE_LAST_USED_MS = "( ?1 - " +
208                Tables.SMARTDIAL_TABLE + "." + SmartDialDbColumns.LAST_TIME_USED + ")";
209
210        /** Contacts that have been used in the past 3 days rank higher than contacts that have
211         * been used in the past 30 days, which rank higher than contacts that have not been used
212         * in recent 30 days.
213         */
214        static final String SORT_BY_DATA_USAGE =
215                "(CASE WHEN " + TIME_SINCE_LAST_USED_MS + " < " + LAST_TIME_USED_CURRENT_MS +
216                " THEN 0 " +
217                " WHEN " + TIME_SINCE_LAST_USED_MS + " < " + LAST_TIME_USED_RECENT_MS +
218                " THEN 1 " +
219                " ELSE 2 END)";
220
221        /** This sort order is similar to that used by the ContactsProvider when returning a list
222         * of frequently called contacts.
223         */
224        static final String SORT_ORDER =
225                Tables.SMARTDIAL_TABLE + "." + SmartDialDbColumns.STARRED + " DESC, "
226                + Tables.SMARTDIAL_TABLE + "." + SmartDialDbColumns.IS_SUPER_PRIMARY + " DESC, "
227                + SORT_BY_DATA_USAGE + ", "
228                + Tables.SMARTDIAL_TABLE + "." + SmartDialDbColumns.TIMES_USED + " DESC, "
229                + Tables.SMARTDIAL_TABLE + "." + SmartDialDbColumns.IN_VISIBLE_GROUP + " DESC, "
230                + Tables.SMARTDIAL_TABLE + "." + SmartDialDbColumns.DISPLAY_NAME_PRIMARY + ", "
231                + Tables.SMARTDIAL_TABLE + "." + SmartDialDbColumns.CONTACT_ID + ", "
232                + Tables.SMARTDIAL_TABLE + "." + SmartDialDbColumns.IS_PRIMARY + " DESC";
233    }
234
235    /**
236     * Simple data format for a contact, containing only information needed for showing up in
237     * smart dial interface.
238     */
239    public static class ContactNumber {
240        public final long id;
241        public final long dataId;
242        public final String displayName;
243        public final String phoneNumber;
244        public final String lookupKey;
245        public final long photoId;
246
247        public ContactNumber(long id, long dataID, String displayName, String phoneNumber,
248                String lookupKey, long photoId) {
249            this.dataId = dataID;
250            this.id = id;
251            this.displayName = displayName;
252            this.phoneNumber = phoneNumber;
253            this.lookupKey = lookupKey;
254            this.photoId = photoId;
255        }
256
257        @Override
258        public int hashCode() {
259            return Objects.hashCode(id, dataId, displayName, phoneNumber, lookupKey, photoId);
260        }
261
262        @Override
263        public boolean equals(Object object) {
264            if (this == object) {
265                return true;
266            }
267            if (object instanceof ContactNumber) {
268                final ContactNumber that = (ContactNumber) object;
269                return Objects.equal(this.id, that.id)
270                        && Objects.equal(this.dataId, that.dataId)
271                        && Objects.equal(this.displayName, that.displayName)
272                        && Objects.equal(this.phoneNumber, that.phoneNumber)
273                        && Objects.equal(this.lookupKey, that.lookupKey)
274                        && Objects.equal(this.photoId, that.photoId);
275            }
276            return false;
277        }
278    }
279
280    /**
281     * Data format for finding duplicated contacts.
282     */
283    private class ContactMatch {
284        private final String lookupKey;
285        private final long id;
286
287        public ContactMatch(String lookupKey, long id) {
288            this.lookupKey = lookupKey;
289            this.id = id;
290        }
291
292        @Override
293        public int hashCode() {
294            return Objects.hashCode(lookupKey, id);
295        }
296
297        @Override
298        public boolean equals(Object object) {
299            if (this == object) {
300                return true;
301            }
302            if (object instanceof ContactMatch) {
303                final ContactMatch that = (ContactMatch) object;
304                return Objects.equal(this.lookupKey, that.lookupKey)
305                        && Objects.equal(this.id, that.id);
306            }
307            return false;
308        }
309    }
310
311    /**
312     * Access function to get the singleton instance of DialerDatabaseHelper.
313     */
314    public static synchronized DialerDatabaseHelper getInstance(Context context) {
315        if (DEBUG) {
316            Log.v(TAG, "Getting Instance");
317        }
318        if (sSingleton == null) {
319            // Use application context instead of activity context because this is a singleton,
320            // and we don't want to leak the activity if the activity is not running but the
321            // dialer database helper is still doing work.
322            sSingleton = new DialerDatabaseHelper(context.getApplicationContext(),
323                    DATABASE_NAME);
324        }
325        return sSingleton;
326    }
327
328    /**
329     * Returns a new instance for unit tests. The database will be created in memory.
330     */
331    @VisibleForTesting
332    static DialerDatabaseHelper getNewInstanceForTest(Context context) {
333        return new DialerDatabaseHelper(context, null);
334    }
335
336    protected DialerDatabaseHelper(Context context, String databaseName) {
337        this(context, databaseName, DATABASE_VERSION);
338    }
339
340    protected DialerDatabaseHelper(Context context, String databaseName, int dbVersion) {
341        super(context, databaseName, null, dbVersion);
342        mContext = Preconditions.checkNotNull(context, "Context must not be null");
343    }
344
345    /**
346     * Creates tables in the database when database is created for the first time.
347     *
348     * @param db The database.
349     */
350    @Override
351    public void onCreate(SQLiteDatabase db) {
352        setupTables(db);
353    }
354
355    private void setupTables(SQLiteDatabase db) {
356        dropTables(db);
357        db.execSQL("CREATE TABLE " + Tables.SMARTDIAL_TABLE + " (" +
358                SmartDialDbColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
359                SmartDialDbColumns.DATA_ID + " INTEGER, " +
360                SmartDialDbColumns.NUMBER + " TEXT," +
361                SmartDialDbColumns.CONTACT_ID + " INTEGER," +
362                SmartDialDbColumns.LOOKUP_KEY + " TEXT," +
363                SmartDialDbColumns.DISPLAY_NAME_PRIMARY + " TEXT, " +
364                SmartDialDbColumns.PHOTO_ID + " INTEGER, " +
365                SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME + " LONG, " +
366                SmartDialDbColumns.LAST_TIME_USED + " LONG, " +
367                SmartDialDbColumns.TIMES_USED + " INTEGER, " +
368                SmartDialDbColumns.STARRED + " INTEGER, " +
369                SmartDialDbColumns.IS_SUPER_PRIMARY + " INTEGER, " +
370                SmartDialDbColumns.IN_VISIBLE_GROUP + " INTEGER, " +
371                SmartDialDbColumns.IS_PRIMARY + " INTEGER" +
372        ");");
373
374        db.execSQL("CREATE TABLE " + Tables.PREFIX_TABLE + " (" +
375                PrefixColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
376                PrefixColumns.PREFIX + " TEXT COLLATE NOCASE, " +
377                PrefixColumns.CONTACT_ID + " INTEGER" +
378                ");");
379
380        db.execSQL("CREATE TABLE " + Tables.PROPERTIES + " (" +
381                PropertiesColumns.PROPERTY_KEY + " TEXT PRIMARY KEY, " +
382                PropertiesColumns.PROPERTY_VALUE + " TEXT " +
383                ");");
384
385        setProperty(db, DATABASE_VERSION_PROPERTY, String.valueOf(DATABASE_VERSION));
386        resetSmartDialLastUpdatedTime();
387    }
388
389    public void dropTables(SQLiteDatabase db) {
390        db.execSQL("DROP TABLE IF EXISTS " + Tables.PREFIX_TABLE);
391        db.execSQL("DROP TABLE IF EXISTS " + Tables.SMARTDIAL_TABLE);
392        db.execSQL("DROP TABLE IF EXISTS " + Tables.PROPERTIES);
393    }
394
395    @Override
396    public void onUpgrade(SQLiteDatabase db, int oldNumber, int newNumber) {
397        // Disregard the old version and new versions provided by SQLiteOpenHelper, we will read
398        // our own from the database.
399
400        int oldVersion;
401
402        oldVersion = getPropertyAsInt(db, DATABASE_VERSION_PROPERTY, 0);
403
404        if (oldVersion == 0) {
405            Log.e(TAG, "Malformed database version..recreating database");
406        }
407
408        if (oldVersion < 4) {
409            setupTables(db);
410            return;
411        }
412
413        if (oldVersion != DATABASE_VERSION) {
414            throw new IllegalStateException(
415                    "error upgrading the database to version " + DATABASE_VERSION);
416        }
417
418        setProperty(db, DATABASE_VERSION_PROPERTY, String.valueOf(DATABASE_VERSION));
419    }
420
421    /**
422     * Stores a key-value pair in the {@link Tables#PROPERTIES} table.
423     */
424    public void setProperty(String key, String value) {
425        setProperty(getWritableDatabase(), key, value);
426    }
427
428    public void setProperty(SQLiteDatabase db, String key, String value) {
429        final ContentValues values = new ContentValues();
430        values.put(PropertiesColumns.PROPERTY_KEY, key);
431        values.put(PropertiesColumns.PROPERTY_VALUE, value);
432        db.replace(Tables.PROPERTIES, null, values);
433    }
434
435    /**
436     * Returns the value from the {@link Tables#PROPERTIES} table.
437     */
438    public String getProperty(String key, String defaultValue) {
439        return getProperty(getReadableDatabase(), key, defaultValue);
440    }
441
442    public String getProperty(SQLiteDatabase db, String key, String defaultValue) {
443        try {
444            final Cursor cursor = db.query(Tables.PROPERTIES,
445                    new String[] {PropertiesColumns.PROPERTY_VALUE},
446                            PropertiesColumns.PROPERTY_KEY + "=?",
447                    new String[] {key}, null, null, null);
448            String value = null;
449            try {
450                if (cursor.moveToFirst()) {
451                    value = cursor.getString(0);
452                }
453            } finally {
454                cursor.close();
455            }
456            return value != null ? value : defaultValue;
457        } catch (SQLiteException e) {
458            return defaultValue;
459        }
460    }
461
462    public int getPropertyAsInt(SQLiteDatabase db, String key, int defaultValue) {
463        final String stored = getProperty(db, key, "");
464        try {
465            return Integer.parseInt(stored);
466        } catch (NumberFormatException e) {
467            return defaultValue;
468        }
469    }
470
471    private void resetSmartDialLastUpdatedTime() {
472        final SharedPreferences databaseLastUpdateSharedPref = mContext.getSharedPreferences(
473                DATABASE_LAST_CREATED_SHARED_PREF, Context.MODE_PRIVATE);
474        final SharedPreferences.Editor editor = databaseLastUpdateSharedPref.edit();
475        editor.putLong(LAST_UPDATED_MILLIS, 0);
476        editor.commit();
477    }
478
479    /**
480     * Starts the database upgrade process in the background.
481     */
482    public void startSmartDialUpdateThread() {
483        new SmartDialUpdateAsyncTask().execute();
484    }
485
486    private class SmartDialUpdateAsyncTask extends AsyncTask {
487        @Override
488        protected Object doInBackground(Object[] objects) {
489            if (DEBUG) {
490                Log.v(TAG, "Updating database");
491            }
492            updateSmartDialDatabase();
493            return null;
494        }
495
496        @Override
497        protected void onCancelled() {
498            if (DEBUG) {
499                Log.v(TAG, "Updating Cancelled");
500            }
501            super.onCancelled();
502        }
503
504        @Override
505        protected void onPostExecute(Object o) {
506            if (DEBUG) {
507                Log.v(TAG, "Updating Finished");
508            }
509            super.onPostExecute(o);
510        }
511    }
512    /**
513     * Removes rows in the smartdial database that matches the contacts that have been deleted
514     * by other apps since last update.
515     *
516     * @param db Database pointer to the dialer database.
517     * @param last_update_time Time stamp of last update on the smartdial database
518     */
519    private void removeDeletedContacts(SQLiteDatabase db, String last_update_time) {
520        final Cursor deletedContactCursor = mContext.getContentResolver().query(
521                DeleteContactQuery.URI,
522                DeleteContactQuery.PROJECTION,
523                DeleteContactQuery.SELECT_UPDATED_CLAUSE,
524                new String[] {last_update_time}, null);
525        if (deletedContactCursor == null) {
526            return;
527        }
528
529        db.beginTransaction();
530        try {
531            while (deletedContactCursor.moveToNext()) {
532                final Long deleteContactId =
533                        deletedContactCursor.getLong(DeleteContactQuery.DELETED_CONTACT_ID);
534                db.delete(Tables.SMARTDIAL_TABLE,
535                        SmartDialDbColumns.CONTACT_ID + "=" + deleteContactId, null);
536                db.delete(Tables.PREFIX_TABLE,
537                        PrefixColumns.CONTACT_ID + "=" + deleteContactId, null);
538            }
539
540            db.setTransactionSuccessful();
541        } finally {
542            deletedContactCursor.close();
543            db.endTransaction();
544        }
545    }
546
547    /**
548     * Removes potentially corrupted entries in the database. These contacts may be added before
549     * the previous instance of the dialer was destroyed for some reason. For data integrity, we
550     * delete all of them.
551
552     * @param db Database pointer to the dialer database.
553     * @param last_update_time Time stamp of last successful update of the dialer database.
554     */
555    private void removePotentiallyCorruptedContacts(SQLiteDatabase db, String last_update_time) {
556        db.delete(Tables.PREFIX_TABLE,
557                PrefixColumns.CONTACT_ID + " IN " +
558                "(SELECT " + SmartDialDbColumns.CONTACT_ID + " FROM " + Tables.SMARTDIAL_TABLE +
559                " WHERE " + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME + " > " +
560                last_update_time + ")",
561                null);
562        db.delete(Tables.SMARTDIAL_TABLE,
563                SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME + " > " + last_update_time, null);
564    }
565
566    /**
567     * Removes all entries in the smartdial contact database.
568     */
569    @VisibleForTesting
570    void removeAllContacts(SQLiteDatabase db) {
571        db.delete(Tables.SMARTDIAL_TABLE, null, null);
572        db.delete(Tables.PREFIX_TABLE, null, null);
573    }
574
575    /**
576     * Counts number of rows of the prefix table.
577     */
578    @VisibleForTesting
579    int countPrefixTableRows(SQLiteDatabase db) {
580        return (int)DatabaseUtils.longForQuery(db, "SELECT COUNT(1) FROM " + Tables.PREFIX_TABLE,
581                null);
582    }
583
584    /**
585     * Removes rows in the smartdial database that matches updated contacts.
586     *
587     * @param db Database pointer to the smartdial database
588     * @param updatedContactCursor Cursor pointing to the list of recently updated contacts.
589     */
590    private void removeUpdatedContacts(SQLiteDatabase db, Cursor updatedContactCursor) {
591        db.beginTransaction();
592        try {
593            while (updatedContactCursor.moveToNext()) {
594                final Long contactId = updatedContactCursor.getLong(PhoneQuery.PHONE_CONTACT_ID);
595
596                db.delete(Tables.SMARTDIAL_TABLE, SmartDialDbColumns.CONTACT_ID + "=" +
597                        contactId, null);
598                db.delete(Tables.PREFIX_TABLE, PrefixColumns.CONTACT_ID + "=" +
599                        contactId, null);
600            }
601
602            db.setTransactionSuccessful();
603        } finally {
604            db.endTransaction();
605        }
606    }
607
608    /**
609     * Inserts updated contacts as rows to the smartdial table.
610     *
611     * @param db Database pointer to the smartdial database.
612     * @param updatedContactCursor Cursor pointing to the list of recently updated contacts.
613     * @param currentMillis Current time to be recorded in the smartdial table as update timestamp.
614     */
615    @VisibleForTesting
616    protected void insertUpdatedContactsAndNumberPrefix(SQLiteDatabase db,
617            Cursor updatedContactCursor, Long currentMillis) {
618        db.beginTransaction();
619        try {
620            final String sqlInsert = "INSERT INTO " + Tables.SMARTDIAL_TABLE + " (" +
621                    SmartDialDbColumns.DATA_ID + ", " +
622                    SmartDialDbColumns.NUMBER + ", " +
623                    SmartDialDbColumns.CONTACT_ID + ", " +
624                    SmartDialDbColumns.LOOKUP_KEY + ", " +
625                    SmartDialDbColumns.DISPLAY_NAME_PRIMARY + ", " +
626                    SmartDialDbColumns.PHOTO_ID + ", " +
627                    SmartDialDbColumns.LAST_TIME_USED + ", " +
628                    SmartDialDbColumns.TIMES_USED + ", " +
629                    SmartDialDbColumns.STARRED + ", " +
630                    SmartDialDbColumns.IS_SUPER_PRIMARY + ", " +
631                    SmartDialDbColumns.IN_VISIBLE_GROUP+ ", " +
632                    SmartDialDbColumns.IS_PRIMARY + ", " +
633                    SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME + ") " +
634                    " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
635            final SQLiteStatement insert = db.compileStatement(sqlInsert);
636
637            final String numberSqlInsert = "INSERT INTO " + Tables.PREFIX_TABLE + " (" +
638                    PrefixColumns.CONTACT_ID + ", " +
639                    PrefixColumns.PREFIX  + ") " +
640                    " VALUES (?, ?)";
641            final SQLiteStatement numberInsert = db.compileStatement(numberSqlInsert);
642
643            updatedContactCursor.moveToPosition(-1);
644            while (updatedContactCursor.moveToNext()) {
645                insert.clearBindings();
646
647                // Handle string columns which can possibly be null first. In the case of certain
648                // null columns (due to malformed rows possibly inserted by third-party apps
649                // or sync adapters), skip the phone number row.
650                final String number = updatedContactCursor.getString(PhoneQuery.PHONE_NUMBER);
651                if (TextUtils.isEmpty(number)) {
652                    continue;
653                } else {
654                    insert.bindString(2, number);
655                }
656
657                final String lookupKey = updatedContactCursor.getString(
658                        PhoneQuery.PHONE_LOOKUP_KEY);
659                if (TextUtils.isEmpty(lookupKey)) {
660                    continue;
661                } else {
662                    insert.bindString(4, lookupKey);
663                }
664
665                final String displayName = updatedContactCursor.getString(
666                        PhoneQuery.PHONE_DISPLAY_NAME);
667                if (displayName == null) {
668                    insert.bindString(5, mContext.getResources().getString(R.string.missing_name));
669                } else {
670                    insert.bindString(5, displayName);
671                }
672                insert.bindLong(1, updatedContactCursor.getLong(PhoneQuery.PHONE_ID));
673                insert.bindLong(3, updatedContactCursor.getLong(PhoneQuery.PHONE_CONTACT_ID));
674                insert.bindLong(6, updatedContactCursor.getLong(PhoneQuery.PHONE_PHOTO_ID));
675                insert.bindLong(7, updatedContactCursor.getLong(PhoneQuery.PHONE_LAST_TIME_USED));
676                insert.bindLong(8, updatedContactCursor.getInt(PhoneQuery.PHONE_TIMES_USED));
677                insert.bindLong(9, updatedContactCursor.getInt(PhoneQuery.PHONE_STARRED));
678                insert.bindLong(10, updatedContactCursor.getInt(PhoneQuery.PHONE_IS_SUPER_PRIMARY));
679                insert.bindLong(11, updatedContactCursor.getInt(PhoneQuery.PHONE_IN_VISIBLE_GROUP));
680                insert.bindLong(12, updatedContactCursor.getInt(PhoneQuery.PHONE_IS_PRIMARY));
681                insert.bindLong(13, currentMillis);
682                insert.executeInsert();
683                final String contactPhoneNumber =
684                        updatedContactCursor.getString(PhoneQuery.PHONE_NUMBER);
685                final ArrayList<String> numberPrefixes =
686                        SmartDialPrefix.parseToNumberTokens(contactPhoneNumber);
687
688                for (String numberPrefix : numberPrefixes) {
689                    numberInsert.bindLong(1, updatedContactCursor.getLong(
690                            PhoneQuery.PHONE_CONTACT_ID));
691                    numberInsert.bindString(2, numberPrefix);
692                    numberInsert.executeInsert();
693                    numberInsert.clearBindings();
694                }
695            }
696
697            db.setTransactionSuccessful();
698        } finally {
699            db.endTransaction();
700        }
701    }
702
703    /**
704     * Inserts prefixes of contact names to the prefix table.
705     *
706     * @param db Database pointer to the smartdial database.
707     * @param nameCursor Cursor pointing to the list of distinct updated contacts.
708     */
709    @VisibleForTesting
710    void insertNamePrefixes(SQLiteDatabase db, Cursor nameCursor) {
711        final int columnIndexName = nameCursor.getColumnIndex(
712                SmartDialDbColumns.DISPLAY_NAME_PRIMARY);
713        final int columnIndexContactId = nameCursor.getColumnIndex(SmartDialDbColumns.CONTACT_ID);
714
715        db.beginTransaction();
716        try {
717            final String sqlInsert = "INSERT INTO " + Tables.PREFIX_TABLE + " (" +
718                    PrefixColumns.CONTACT_ID + ", " +
719                    PrefixColumns.PREFIX  + ") " +
720                    " VALUES (?, ?)";
721            final SQLiteStatement insert = db.compileStatement(sqlInsert);
722
723            while (nameCursor.moveToNext()) {
724                /** Computes a list of prefixes of a given contact name. */
725                final ArrayList<String> namePrefixes =
726                        SmartDialPrefix.generateNamePrefixes(nameCursor.getString(columnIndexName));
727
728                for (String namePrefix : namePrefixes) {
729                    insert.bindLong(1, nameCursor.getLong(columnIndexContactId));
730                    insert.bindString(2, namePrefix);
731                    insert.executeInsert();
732                    insert.clearBindings();
733                }
734            }
735
736            db.setTransactionSuccessful();
737        } finally {
738            db.endTransaction();
739        }
740    }
741
742    /**
743     * Updates the smart dial and prefix database.
744     * This method queries the Delta API to get changed contacts since last update, and updates the
745     * records in smartdial database and prefix database accordingly.
746     * It also queries the deleted contact database to remove newly deleted contacts since last
747     * update.
748     */
749    public void updateSmartDialDatabase() {
750        final SQLiteDatabase db = getWritableDatabase();
751
752        synchronized(mLock) {
753            if (DEBUG) {
754                Log.v(TAG, "Starting to update database");
755            }
756            final StopWatch stopWatch = DEBUG ? StopWatch.start("Updating databases") : null;
757
758            /** Gets the last update time on the database. */
759            final SharedPreferences databaseLastUpdateSharedPref = mContext.getSharedPreferences(
760                    DATABASE_LAST_CREATED_SHARED_PREF, Context.MODE_PRIVATE);
761            final String lastUpdateMillis = String.valueOf(
762                    databaseLastUpdateSharedPref.getLong(LAST_UPDATED_MILLIS, 0));
763
764            if (DEBUG) {
765                Log.v(TAG, "Last updated at " + lastUpdateMillis);
766            }
767            /** Queries the contact database to get contacts that have been updated since the last
768             * update time.
769             */
770            final Cursor updatedContactCursor = mContext.getContentResolver().query(PhoneQuery.URI,
771                    PhoneQuery.PROJECTION, PhoneQuery.SELECTION,
772                    new String[]{lastUpdateMillis}, null);
773
774            /** Sets the time after querying the database as the current update time. */
775            final Long currentMillis = System.currentTimeMillis();
776
777            if (DEBUG) {
778                stopWatch.lap("Queried the Contacts database");
779            }
780
781            if (updatedContactCursor == null) {
782                if (DEBUG) {
783                    Log.e(TAG, "SmartDial query received null for cursor");
784                }
785                return;
786            }
787
788            /** Prevents the app from reading the dialer database when updating. */
789            sInUpdate.getAndSet(true);
790
791            /** Removes contacts that have been deleted. */
792            removeDeletedContacts(db, lastUpdateMillis);
793            removePotentiallyCorruptedContacts(db, lastUpdateMillis);
794
795            if (DEBUG) {
796                stopWatch.lap("Finished deleting deleted entries");
797            }
798
799            try {
800                /** If the database did not exist before, jump through deletion as there is nothing
801                 * to delete.
802                 */
803                if (!lastUpdateMillis.equals("0")) {
804                    /** Removes contacts that have been updated. Updated contact information will be
805                     * inserted later.
806                     */
807                    removeUpdatedContacts(db, updatedContactCursor);
808                    if (DEBUG) {
809                        stopWatch.lap("Finished deleting updated entries");
810                    }
811                }
812
813                /** Inserts recently updated contacts to the smartdial database.*/
814                insertUpdatedContactsAndNumberPrefix(db, updatedContactCursor, currentMillis);
815                if (DEBUG) {
816                    stopWatch.lap("Finished building the smart dial table");
817                }
818            } finally {
819                /** Inserts prefixes of phone numbers into the prefix table.*/
820                updatedContactCursor.close();
821            }
822
823            /** Gets a list of distinct contacts which have been updated, and adds the name prefixes
824             * of these contacts to the prefix table.
825             */
826            final Cursor nameCursor = db.rawQuery(
827                    "SELECT DISTINCT " +
828                    SmartDialDbColumns.DISPLAY_NAME_PRIMARY + ", " + SmartDialDbColumns.CONTACT_ID +
829                    " FROM " + Tables.SMARTDIAL_TABLE +
830                    " WHERE " + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME +
831                    " = " + Long.toString(currentMillis),
832                    new String[] {});
833            if (DEBUG) {
834                stopWatch.lap("Queried the smart dial table for contact names");
835            }
836
837            if (nameCursor != null) {
838                try {
839                    /** Inserts prefixes of names into the prefix table.*/
840                    insertNamePrefixes(db, nameCursor);
841                    if (DEBUG) {
842                        stopWatch.lap("Finished building the name prefix table");
843                    }
844                } finally {
845                    nameCursor.close();
846                }
847            }
848
849            /** Creates index on contact_id for fast JOIN operation. */
850            db.execSQL("CREATE INDEX IF NOT EXISTS smartdial_contact_id_index ON " +
851                    Tables.SMARTDIAL_TABLE + " (" + SmartDialDbColumns.CONTACT_ID  + ");");
852            /** Creates index on last_smartdial_update_time for fast SELECT operation. */
853            db.execSQL("CREATE INDEX IF NOT EXISTS smartdial_last_update_index ON " +
854                    Tables.SMARTDIAL_TABLE + " (" +
855                    SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME + ");");
856            /** Creates index on sorting fields for fast sort operation. */
857            db.execSQL("CREATE INDEX IF NOT EXISTS smartdial_sort_index ON " +
858                    Tables.SMARTDIAL_TABLE + " (" +
859                    SmartDialDbColumns.STARRED + ", " +
860                    SmartDialDbColumns.IS_SUPER_PRIMARY + ", " +
861                    SmartDialDbColumns.LAST_TIME_USED + ", " +
862                    SmartDialDbColumns.TIMES_USED + ", " +
863                    SmartDialDbColumns.IN_VISIBLE_GROUP +  ", " +
864                    SmartDialDbColumns.DISPLAY_NAME_PRIMARY + ", " +
865                    SmartDialDbColumns.CONTACT_ID + ", " +
866                    SmartDialDbColumns.IS_PRIMARY +
867                    ");");
868            /** Creates index on prefix for fast SELECT operation. */
869            db.execSQL("CREATE INDEX IF NOT EXISTS nameprefix_index ON " +
870                    Tables.PREFIX_TABLE + " (" + PrefixColumns.PREFIX + ");");
871            /** Creates index on contact_id for fast JOIN operation. */
872            db.execSQL("CREATE INDEX IF NOT EXISTS nameprefix_contact_id_index ON " +
873                    Tables.PREFIX_TABLE + " (" + PrefixColumns.CONTACT_ID + ");");
874
875            if (DEBUG) {
876                stopWatch.lap(TAG + "Finished recreating index");
877            }
878
879            /** Updates the database index statistics.*/
880            db.execSQL("ANALYZE " + Tables.SMARTDIAL_TABLE);
881            db.execSQL("ANALYZE " + Tables.PREFIX_TABLE);
882            db.execSQL("ANALYZE smartdial_contact_id_index");
883            db.execSQL("ANALYZE smartdial_last_update_index");
884            db.execSQL("ANALYZE nameprefix_index");
885            db.execSQL("ANALYZE nameprefix_contact_id_index");
886            if (DEBUG) {
887                stopWatch.stopAndLog(TAG + "Finished updating index stats", 0);
888            }
889
890            sInUpdate.getAndSet(false);
891
892            final SharedPreferences.Editor editor = databaseLastUpdateSharedPref.edit();
893            editor.putLong(LAST_UPDATED_MILLIS, currentMillis);
894            editor.commit();
895        }
896    }
897
898    /**
899     * Returns a list of candidate contacts where the query is a prefix of the dialpad index of
900     * the contact's name or phone number.
901     *
902     * @param query The prefix of a contact's dialpad index.
903     * @return A list of top candidate contacts that will be suggested to user to match their input.
904     */
905    public ArrayList<ContactNumber>  getLooseMatches(String query,
906            SmartDialNameMatcher nameMatcher) {
907        final boolean inUpdate = sInUpdate.get();
908        if (inUpdate) {
909            return Lists.newArrayList();
910        }
911
912        final SQLiteDatabase db = getReadableDatabase();
913
914        /** Uses SQL query wildcard '%' to represent prefix matching.*/
915        final String looseQuery = query + "%";
916
917        final ArrayList<ContactNumber> result = Lists.newArrayList();
918
919        final StopWatch stopWatch = DEBUG ? StopWatch.start(":Name Prefix query") : null;
920
921        final String currentTimeStamp = Long.toString(System.currentTimeMillis());
922
923        /** Queries the database to find contacts that have an index matching the query prefix. */
924        final Cursor cursor = db.rawQuery("SELECT " +
925                SmartDialDbColumns.DATA_ID + ", " +
926                SmartDialDbColumns.DISPLAY_NAME_PRIMARY + ", " +
927                SmartDialDbColumns.PHOTO_ID + ", " +
928                SmartDialDbColumns.NUMBER + ", " +
929                SmartDialDbColumns.CONTACT_ID + ", " +
930                SmartDialDbColumns.LOOKUP_KEY +
931                " FROM " + Tables.SMARTDIAL_TABLE + " WHERE " +
932                SmartDialDbColumns.CONTACT_ID + " IN " +
933                    " (SELECT " + PrefixColumns.CONTACT_ID +
934                    " FROM " + Tables.PREFIX_TABLE +
935                    " WHERE " + Tables.PREFIX_TABLE + "." + PrefixColumns.PREFIX +
936                    " LIKE '" + looseQuery + "')" +
937                " ORDER BY " + SmartDialSortingOrder.SORT_ORDER,
938                new String[] {currentTimeStamp});
939
940        if (DEBUG) {
941            stopWatch.lap("Prefix query completed");
942        }
943
944        /** Gets the column ID from the cursor.*/
945        final int columnDataId = 0;
946        final int columnDisplayNamePrimary = 1;
947        final int columnPhotoId = 2;
948        final int columnNumber = 3;
949        final int columnId = 4;
950        final int columnLookupKey = 5;
951        if (DEBUG) {
952            stopWatch.lap("Found column IDs");
953        }
954
955        final Set<ContactMatch> duplicates = new HashSet<ContactMatch>();
956        int counter = 0;
957        try {
958            if (DEBUG) {
959                stopWatch.lap("Moved cursor to start");
960            }
961            /** Iterates the cursor to find top contact suggestions without duplication.*/
962            while ((cursor.moveToNext()) && (counter < MAX_ENTRIES)) {
963                final long dataID = cursor.getLong(columnDataId);
964                final String displayName = cursor.getString(columnDisplayNamePrimary);
965                final String phoneNumber = cursor.getString(columnNumber);
966                final long id = cursor.getLong(columnId);
967                final long photoId = cursor.getLong(columnPhotoId);
968                final String lookupKey = cursor.getString(columnLookupKey);
969
970                /** If a contact already exists and another phone number of the contact is being
971                 * processed, skip the second instance.
972                 */
973                final ContactMatch contactMatch = new ContactMatch(lookupKey, id);
974                if (duplicates.contains(contactMatch)) {
975                    continue;
976                }
977
978                /**
979                 * If the contact has either the name or number that matches the query, add to the
980                 * result.
981                 */
982                final boolean nameMatches = nameMatcher.matches(displayName);
983                final boolean numberMatches =
984                        (nameMatcher.matchesNumber(phoneNumber, query) != null);
985                if (nameMatches || numberMatches) {
986                    /** If a contact has not been added, add it to the result and the hash set.*/
987                    duplicates.add(contactMatch);
988                    result.add(new ContactNumber(id, dataID, displayName, phoneNumber, lookupKey,
989                            photoId));
990                    counter++;
991                    if (DEBUG) {
992                        stopWatch.lap("Added one result");
993                    }
994                }
995            }
996
997            if (DEBUG) {
998                stopWatch.stopAndLog(TAG + "Finished loading cursor", 0);
999            }
1000        } finally {
1001            cursor.close();
1002        }
1003        return result;
1004    }
1005}
1006