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