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 
17 package com.android.providers.contacts.database;
18 
19 import static android.provider.ContactsContract.Contacts;
20 import static com.android.providers.contacts.ContactsDatabaseHelper.Tables;
21 
22 import android.content.ContentValues;
23 import android.database.Cursor;
24 import android.database.sqlite.SQLiteDatabase;
25 import android.provider.ContactsContract;
26 import android.text.TextUtils;
27 
28 import com.android.common.io.MoreCloseables;
29 import com.android.providers.contacts.util.Clock;
30 
31 import java.util.Set;
32 
33 /**
34  * Methods for operating on the contacts table.
35  */
36 public class ContactsTableUtil {
37 
38     /**
39      * Drop indexes if present.  Create indexes.
40      *
41      * @param db The sqlite database instance.
42      */
createIndexes(SQLiteDatabase db)43     public static void createIndexes(SQLiteDatabase db) {
44         final String table = Tables.CONTACTS;
45 
46         db.execSQL("CREATE INDEX contacts_has_phone_index ON " + table + " (" +
47                 Contacts.HAS_PHONE_NUMBER +
48                 ");");
49 
50         db.execSQL("CREATE INDEX contacts_name_raw_contact_id_index ON " + table + " (" +
51                 Contacts.NAME_RAW_CONTACT_ID +
52                 ");");
53 
54         db.execSQL(MoreDatabaseUtils.buildCreateIndexSql(table,
55                 Contacts.CONTACT_LAST_UPDATED_TIMESTAMP));
56     }
57 
updateContactLastUpdateByContactId(SQLiteDatabase db, long contactId)58     public static void updateContactLastUpdateByContactId(SQLiteDatabase db, long contactId) {
59         final ContentValues values = new ContentValues();
60         values.put(Contacts.CONTACT_LAST_UPDATED_TIMESTAMP,
61                 Clock.getInstance().currentTimeMillis());
62         db.update(Tables.CONTACTS, values, Contacts._ID + " = ?",
63                 new String[] {String.valueOf(contactId)});
64     }
65 
66     /**
67      * Refreshes the last updated timestamp of the contact with the current time.
68      *
69      * @param db The sqlite database instance.
70      * @param rawContactIds A set of raw contacts ids to refresh the contact for.
71      */
updateContactLastUpdateByRawContactId(SQLiteDatabase db, Set<Long> rawContactIds)72     public static void updateContactLastUpdateByRawContactId(SQLiteDatabase db,
73             Set<Long> rawContactIds) {
74         if (rawContactIds.isEmpty()) {
75             return;
76         }
77 
78         db.execSQL(buildUpdateLastUpdateSql(rawContactIds));
79     }
80 
81     /**
82      * Build a sql to update the last updated timestamp for contacts.
83      *
84      * @param rawContactIds The raw contact ids that contacts should be updated for.
85      * @return The update sql statement.
86      */
buildUpdateLastUpdateSql(Set<Long> rawContactIds)87     private static String buildUpdateLastUpdateSql(Set<Long> rawContactIds) {
88         // Not using bind args here due to sqlite bind arg size limit.  Large number of bind args
89         // will cause a sqlite error:
90         //     android.database.sqlite.SQLiteException: too many SQL variables (code 1)
91         // Sql injection is not possible because input is a set of Long.  If any part of the sql
92         // is built with user input strings, then this must be converted to using bind args.
93         final String sql = "UPDATE " + Tables.CONTACTS
94                 + " SET " + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + " = "
95                 + Clock.getInstance().currentTimeMillis()
96                 + " WHERE " + Contacts._ID + " IN ( "
97                 + "  SELECT " + ContactsContract.RawContacts.CONTACT_ID
98                 + "  FROM " + Tables.RAW_CONTACTS
99                 + "  WHERE " + ContactsContract.RawContacts._ID
100                 + " IN (" + TextUtils.join(",", rawContactIds) + ") "
101                 + ")";
102         return sql;
103     }
104 
105     /**
106      * Delete a contact identified by the contact id.
107      *
108      * @param db The sqlite database instance.
109      * @param contactId The contact id to delete.
110      * @return The number of records deleted.
111      */
deleteContact(SQLiteDatabase db, long contactId)112     public static int deleteContact(SQLiteDatabase db, long contactId) {
113         DeletedContactsTableUtil.insertDeletedContact(db, contactId);
114         return db.delete(Tables.CONTACTS, Contacts._ID + " = ?", new String[]{contactId + ""});
115     }
116 
117     /**
118      * Delete the aggregate contact if it has no constituent raw contacts other than the supplied
119      * one.
120      */
deleteContactIfSingleton(SQLiteDatabase db, long rawContactId)121     public static void deleteContactIfSingleton(SQLiteDatabase db, long rawContactId) {
122         // This query will find a contact id if the contact has a raw contacts other than the one
123         // passed in.
124         final String sql = "select " + ContactsContract.RawContacts.CONTACT_ID + ", count(1)"
125                 + " from " + Tables.RAW_CONTACTS
126                 + " where " + ContactsContract.RawContacts.CONTACT_ID + " ="
127                 + "  (select " + ContactsContract.RawContacts.CONTACT_ID
128                 + "   from " + Tables.RAW_CONTACTS
129                 + "   where " + ContactsContract.RawContacts._ID + " = ?)"
130                 + " group by " + ContactsContract.RawContacts.CONTACT_ID;
131         final Cursor cursor = db.rawQuery(sql, new String[]{rawContactId + ""});
132         try {
133             if (cursor.moveToNext()) {
134                 long contactId = cursor.getLong(0);
135                 long numRawContacts = cursor.getLong(1);
136 
137                 if (numRawContacts == 1) {
138                     // Only one raw contact, we can delete the parent.
139                     deleteContact(db, contactId);
140                 }
141             }
142         } finally {
143             MoreCloseables.closeQuietly(cursor);
144         }
145     }
146 }
147