ContactsProvider2.java revision 524913c66ce75ca8dec127ac88e3bc2249c246d9
128c251b54b0b311749f07babe0f6909e71e877bcDaniel Dunbar/* 2fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar * Copyright (C) 2009 The Android Open Source Project 3fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar * 4fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar * Licensed under the Apache License, Version 2.0 (the "License"); 5fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar * you may not use this file except in compliance with the License. 6fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar * You may obtain a copy of the License at 7fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar * 8fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar * http://www.apache.org/licenses/LICENSE-2.0 9fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar * 1028c251b54b0b311749f07babe0f6909e71e877bcDaniel Dunbar * Unless required by applicable law or agreed to in writing, software 1128c251b54b0b311749f07babe0f6909e71e877bcDaniel Dunbar * distributed under the License is distributed on an "AS IS" BASIS, 12fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1385f2ecc697a8ca6c8cf08093054cbbb9d2060ccfRafael Espindola * See the License for the specific language governing permissions and 14fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar * limitations under the License 151f6efa3996dd1929fbc129203ce5009b620e6969Michael J. Spencer */ 16fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar 17fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbarpackage com.android.providers.contacts; 18684c593d05db0bd277268fc9d8c05bce138c745aChris Lattner 19f82f4490b130eca55b08d605456a4ceacccf288aDaniel Dunbarimport com.android.common.content.SQLiteContentProvider; 2032a006e606742b1c5401e49607e33717bb5441f0Rafael Espindolaimport com.android.common.content.SyncStateContentProviderHelper; 21fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbarimport com.android.providers.contacts.ContactAggregator.AggregationSuggestionParameter; 220eab5c4d85b4c4bb161bcdd959aa58a6f54415ccDaniel Dunbarimport com.android.providers.contacts.ContactLookupKey.LookupKeySegment; 2385f2ecc697a8ca6c8cf08093054cbbb9d2060ccfRafael Espindolaimport com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns; 24fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbarimport com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns; 2515d170709608e2f1efcada74c297c10c8c71fdcfDaniel Dunbarimport com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns; 2687392fde1f261fea161b48886fafbedddb18dcceDaniel Dunbarimport com.android.providers.contacts.ContactsDatabaseHelper.Clauses; 274f3e7aa154577c86791908e73a9fec075fdea0baChris Lattnerimport com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns; 2885f2ecc697a8ca6c8cf08093054cbbb9d2060ccfRafael Espindolaimport com.android.providers.contacts.ContactsDatabaseHelper.ContactsStatusUpdatesColumns; 29fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbarimport com.android.providers.contacts.ContactsDatabaseHelper.DataColumns; 3028c251b54b0b311749f07babe0f6909e71e877bcDaniel Dunbarimport com.android.providers.contacts.ContactsDatabaseHelper.DataUsageStatColumns; 319643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns; 3228c251b54b0b311749f07babe0f6909e71e877bcDaniel Dunbarimport com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns; 33fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbarimport com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns; 3428c251b54b0b311749f07babe0f6909e71e877bcDaniel Dunbarimport com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType; 351aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbarimport com.android.providers.contacts.ContactsDatabaseHelper.PhoneColumns; 361aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbarimport com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns; 371aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbarimport com.android.providers.contacts.ContactsDatabaseHelper.PhotoFilesColumns; 385d917a8952c09a345180ec36f0df4ee5dd5eddeaChris Lattnerimport com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns; 395d917a8952c09a345180ec36f0df4ee5dd5eddeaChris Lattnerimport com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns; 40fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbarimport com.android.providers.contacts.ContactsDatabaseHelper.SearchIndexColumns; 419643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport com.android.providers.contacts.ContactsDatabaseHelper.SettingsColumns; 42fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbarimport com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns; 4328c251b54b0b311749f07babe0f6909e71e877bcDaniel Dunbarimport com.android.providers.contacts.ContactsDatabaseHelper.StreamItemPhotosColumns; 449643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport com.android.providers.contacts.ContactsDatabaseHelper.StreamItemsColumns; 459643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport com.android.providers.contacts.ContactsDatabaseHelper.Tables; 469643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport com.android.providers.contacts.ContactsDatabaseHelper.Views; 479643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport com.android.providers.contacts.util.DbQueryUtils; 48d076482ab7e672d1d65a43809695e8d0d3995203Rafael Espindolaimport com.android.vcard.VCardComposer; 49d076482ab7e672d1d65a43809695e8d0d3995203Rafael Espindolaimport com.android.vcard.VCardConfig; 5085f2ecc697a8ca6c8cf08093054cbbb9d2060ccfRafael Espindolaimport com.google.android.collect.Lists; 51fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbarimport com.google.android.collect.Maps; 52159f527cb269002de85e671023b9231a2c8792e9Dan Gohmanimport com.google.android.collect.Sets; 539643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport com.google.common.annotations.VisibleForTesting; 54d076482ab7e672d1d65a43809695e8d0d3995203Rafael Espindola 55d076482ab7e672d1d65a43809695e8d0d3995203Rafael Espindolaimport android.accounts.Account; 5685f2ecc697a8ca6c8cf08093054cbbb9d2060ccfRafael Espindolaimport android.accounts.AccountManager; 57f230df9af4012f9510de664b6d62b128e26a5861Rafael Espindolaimport android.accounts.OnAccountsUpdateListener; 58fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbarimport android.app.Notification; 599643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport android.app.NotificationManager; 609643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport android.app.PendingIntent; 61fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbarimport android.app.SearchManager; 6228c251b54b0b311749f07babe0f6909e71e877bcDaniel Dunbarimport android.content.ContentProviderOperation; 63fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbarimport android.content.ContentProviderResult; 649643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport android.content.ContentResolver; 6587392fde1f261fea161b48886fafbedddb18dcceDaniel Dunbarimport android.content.ContentUris; 6687392fde1f261fea161b48886fafbedddb18dcceDaniel Dunbarimport android.content.ContentValues; 6787392fde1f261fea161b48886fafbedddb18dcceDaniel Dunbarimport android.content.Context; 688cb9a3b13f3226b7e741768b69d26ecd6b5231f1Chris Lattnerimport android.content.IContentService; 6987392fde1f261fea161b48886fafbedddb18dcceDaniel Dunbarimport android.content.Intent; 7087392fde1f261fea161b48886fafbedddb18dcceDaniel Dunbarimport android.content.OperationApplicationException; 7187392fde1f261fea161b48886fafbedddb18dcceDaniel Dunbarimport android.content.SharedPreferences; 729643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport android.content.SyncAdapterType; 739643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport android.content.UriMatcher; 749643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport android.content.res.AssetFileDescriptor; 75fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbarimport android.content.res.Resources; 76fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbarimport android.database.CrossProcessCursor; 7715d170709608e2f1efcada74c297c10c8c71fdcfDaniel Dunbarimport android.database.Cursor; 78f82f4490b130eca55b08d605456a4ceacccf288aDaniel Dunbarimport android.database.CursorWindow; 79f82f4490b130eca55b08d605456a4ceacccf288aDaniel Dunbarimport android.database.CursorWrapper; 80f82f4490b130eca55b08d605456a4ceacccf288aDaniel Dunbarimport android.database.DatabaseUtils; 81fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbarimport android.database.MatrixCursor; 82d076482ab7e672d1d65a43809695e8d0d3995203Rafael Espindolaimport android.database.MatrixCursor.RowBuilder; 83d076482ab7e672d1d65a43809695e8d0d3995203Rafael Espindolaimport android.database.sqlite.SQLiteDatabase; 84d076482ab7e672d1d65a43809695e8d0d3995203Rafael Espindolaimport android.database.sqlite.SQLiteDoneException; 8585f2ecc697a8ca6c8cf08093054cbbb9d2060ccfRafael Espindolaimport android.database.sqlite.SQLiteQueryBuilder; 86d076482ab7e672d1d65a43809695e8d0d3995203Rafael Espindolaimport android.graphics.Bitmap; 87fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbarimport android.graphics.BitmapFactory; 8815d170709608e2f1efcada74c297c10c8c71fdcfDaniel Dunbarimport android.net.Uri; 891aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbarimport android.net.Uri.Builder; 9015d170709608e2f1efcada74c297c10c8c71fdcfDaniel Dunbarimport android.os.Binder; 9115d170709608e2f1efcada74c297c10c8c71fdcfDaniel Dunbarimport android.os.Bundle; 92f82f4490b130eca55b08d605456a4ceacccf288aDaniel Dunbarimport android.os.Handler; 9315d170709608e2f1efcada74c297c10c8c71fdcfDaniel Dunbarimport android.os.HandlerThread; 9433a38a1b5a35acd15c867193bc2175f3d4e7b83dRafael Espindolaimport android.os.Message; 9515d170709608e2f1efcada74c297c10c8c71fdcfDaniel Dunbarimport android.os.ParcelFileDescriptor; 960eab5c4d85b4c4bb161bcdd959aa58a6f54415ccDaniel Dunbarimport android.os.Process; 970eab5c4d85b4c4bb161bcdd959aa58a6f54415ccDaniel Dunbarimport android.os.RemoteException; 980eab5c4d85b4c4bb161bcdd959aa58a6f54415ccDaniel Dunbarimport android.os.StrictMode; 990eab5c4d85b4c4bb161bcdd959aa58a6f54415ccDaniel Dunbarimport android.os.SystemClock; 1000eab5c4d85b4c4bb161bcdd959aa58a6f54415ccDaniel Dunbarimport android.os.SystemProperties; 1010eab5c4d85b4c4bb161bcdd959aa58a6f54415ccDaniel Dunbarimport android.preference.PreferenceManager; 1029643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport android.provider.BaseColumns; 1039643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport android.provider.ContactsContract; 10428c251b54b0b311749f07babe0f6909e71e877bcDaniel Dunbarimport android.provider.ContactsContract.AggregationExceptions; 105fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbarimport android.provider.ContactsContract.CommonDataKinds.Email; 106a0e36d55c495b3325805c659ac365b5faea84e34Daniel Dunbarimport android.provider.ContactsContract.CommonDataKinds.GroupMembership; 1078cb9a3b13f3226b7e741768b69d26ecd6b5231f1Chris Lattnerimport android.provider.ContactsContract.CommonDataKinds.Im; 1088cb9a3b13f3226b7e741768b69d26ecd6b5231f1Chris Lattnerimport android.provider.ContactsContract.CommonDataKinds.Nickname; 1098cb9a3b13f3226b7e741768b69d26ecd6b5231f1Chris Lattnerimport android.provider.ContactsContract.CommonDataKinds.Note; 1108cb9a3b13f3226b7e741768b69d26ecd6b5231f1Chris Lattnerimport android.provider.ContactsContract.CommonDataKinds.Organization; 111fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbarimport android.provider.ContactsContract.CommonDataKinds.Phone; 11228c251b54b0b311749f07babe0f6909e71e877bcDaniel Dunbarimport android.provider.ContactsContract.CommonDataKinds.Photo; 11328c251b54b0b311749f07babe0f6909e71e877bcDaniel Dunbarimport android.provider.ContactsContract.CommonDataKinds.SipAddress; 114fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbarimport android.provider.ContactsContract.CommonDataKinds.StructuredName; 115fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbarimport android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 116159f527cb269002de85e671023b9231a2c8792e9Dan Gohmanimport android.provider.ContactsContract.ContactCounts; 11728c251b54b0b311749f07babe0f6909e71e877bcDaniel Dunbarimport android.provider.ContactsContract.Contacts; 1189643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport android.provider.ContactsContract.Contacts.AggregationSuggestions; 1199643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport android.provider.ContactsContract.Data; 1209643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport android.provider.ContactsContract.DataUsageFeedback; 1219643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport android.provider.ContactsContract.Directory; 1229643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport android.provider.ContactsContract.DisplayPhoto; 1239643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport android.provider.ContactsContract.Groups; 1249643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport android.provider.ContactsContract.Intents; 1259643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport android.provider.ContactsContract.PhoneLookup; 1269643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport android.provider.ContactsContract.PhotoFiles; 1279643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport android.provider.ContactsContract.ProviderStatus; 1289643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport android.provider.ContactsContract.RawContacts; 129fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbarimport android.provider.ContactsContract.SearchSnippetColumns; 130fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbarimport android.provider.ContactsContract.Settings; 1319643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport android.provider.ContactsContract.StatusUpdates; 1329643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport android.provider.ContactsContract.StreamItemPhotos; 1339643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport android.provider.ContactsContract.StreamItems; 1349643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbarimport android.provider.LiveFolders; 135fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbarimport android.provider.OpenableColumns; 13628c251b54b0b311749f07babe0f6909e71e877bcDaniel Dunbarimport android.provider.SyncStateContract; 137fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbarimport android.telephony.PhoneNumberUtils; 138fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbarimport android.telephony.TelephonyManager; 13928c251b54b0b311749f07babe0f6909e71e877bcDaniel Dunbarimport android.text.TextUtils; 1401aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbarimport android.util.Log; 1411aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbar 1421aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbarimport java.io.BufferedWriter; 1431aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbarimport java.io.ByteArrayOutputStream; 1441aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbarimport java.io.File; 14528c251b54b0b311749f07babe0f6909e71e877bcDaniel Dunbarimport java.io.FileNotFoundException; 1464e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbarimport java.io.IOException; 1474e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbarimport java.io.OutputStream; 1484e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbarimport java.io.OutputStreamWriter; 1494e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbarimport java.io.Writer; 1504e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbarimport java.text.SimpleDateFormat; 1514e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbarimport java.util.ArrayList; 1524e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbarimport java.util.Arrays; 1534e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbarimport java.util.Collections; 1544e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbarimport java.util.Date; 1554e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbarimport java.util.HashMap; 1564e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbarimport java.util.HashSet; 157a0a2f8734cdfc19d44201b791a969bcdda96bb70Rafael Espindolaimport java.util.List; 1584e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbarimport java.util.Locale; 1594e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbarimport java.util.Map; 160b4d1721eff7b43577e5f2e53f885973fb6c43683Rafael Espindolaimport java.util.Set; 161a264f72d3fb9dec1427480fcf17ef3c746ea723aRafael Espindolaimport java.util.concurrent.CountDownLatch; 16218c1021ec108722506125926087b1e5fcfb28046Rafael Espindola 1630cf15d61b7e3bf53f5a99f58ada37b93bc039559Rafael Espindola/** 1646135a96792ca05f6366e5dbaee6208e84589c47fChris Lattner * Contacts content provider. The contract between this provider and applications 1652c4d5125c708bb35140fc2a40b02beb1add101dbJim Grosbach * is defined in {@link ContactsContract}. 1662c4d5125c708bb35140fc2a40b02beb1add101dbJim Grosbach */ 1672c4d5125c708bb35140fc2a40b02beb1add101dbJim Grosbachpublic class ContactsProvider2 extends SQLiteContentProvider implements OnAccountsUpdateListener { 1682c4d5125c708bb35140fc2a40b02beb1add101dbJim Grosbach 1692c4d5125c708bb35140fc2a40b02beb1add101dbJim Grosbach private static final String TAG = "ContactsProvider"; 1702c4d5125c708bb35140fc2a40b02beb1add101dbJim Grosbach 1712c4d5125c708bb35140fc2a40b02beb1add101dbJim Grosbach private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE); 172c67b1a3a76ea6ff642d30610cf63595ccb661e6bBob Wilson 1731e61e69d401045c54b15815f15a0fdb3ca56a9b5Chris Lattner private static final int BACKGROUND_TASK_INITIALIZE = 0; 174a1000742d28f33dd8dd9858e64282e7749c0bd64Roman Divacky private static final int BACKGROUND_TASK_OPEN_WRITE_ACCESS = 1; 175a1000742d28f33dd8dd9858e64282e7749c0bd64Roman Divacky private static final int BACKGROUND_TASK_IMPORT_LEGACY_CONTACTS = 2; 176a1000742d28f33dd8dd9858e64282e7749c0bd64Roman Divacky private static final int BACKGROUND_TASK_UPDATE_ACCOUNTS = 3; 177a1000742d28f33dd8dd9858e64282e7749c0bd64Roman Divacky private static final int BACKGROUND_TASK_UPDATE_LOCALE = 4; 1784e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar private static final int BACKGROUND_TASK_UPGRADE_AGGREGATION_ALGORITHM = 5; 1794e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar private static final int BACKGROUND_TASK_UPDATE_SEARCH_INDEX = 6; 1804e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar private static final int BACKGROUND_TASK_UPDATE_PROVIDER_STATUS = 7; 1814e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar private static final int BACKGROUND_TASK_UPDATE_DIRECTORIES = 8; 1829643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int BACKGROUND_TASK_CHANGE_LOCALE = 9; 183fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar private static final int BACKGROUND_TASK_CLEANUP_PHOTOS = 10; 1844e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar 1854e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar /** Default for the maximum number of returned aggregation suggestions. */ 1864e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar private static final int DEFAULT_MAX_SUGGESTIONS = 5; 1874e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar 1884e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar /** Limit for the maximum number of social stream items to store under a raw contact. */ 189fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar private static final int MAX_STREAM_ITEMS_PER_RAW_CONTACT = 5; 1909643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar 1919643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar /** Rate limit (in ms) for photo cleanup. Do it at most once per day. */ 1929643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int PHOTO_CLEANUP_RATE_LIMIT = 24 * 60 * 60 * 1000; 1939643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar 1944e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar /** 1954e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar * Property key for the legacy contact import version. The need for a version 1964e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar * as opposed to a boolean flag is that if we discover bugs in the contact import process, 1974e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar * we can trigger re-import by incrementing the import version. 1984e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar */ 1994e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar private static final String PROPERTY_CONTACTS_IMPORTED = "contacts_imported_v1"; 2004e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar private static final int PROPERTY_CONTACTS_IMPORT_VERSION = 1; 2014e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar private static final String PREF_LOCALE = "locale"; 202c67b1a3a76ea6ff642d30610cf63595ccb661e6bBob Wilson 2039643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final String PROPERTY_AGGREGATION_ALGORITHM = "aggregation_v2"; 2049643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int PROPERTY_AGGREGATION_ALGORITHM_VERSION = 2; 2059643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar 2069643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final String AGGREGATE_CONTACTS = "sync.contacts.aggregate"; 2079643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar 2089643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 2094e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar 2104e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar /** 2114e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar * Used to insert a column into strequent results, which enables SQL to sort the list using 2124e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar * the total times contacted. See also {@link #sStrequentFrequentProjectionMap}. 2134e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar */ 2144e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar private static final String TIMES_USED_SORT_COLUMN = "times_used_sort"; 2154e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar 2164e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar private static final String STREQUENT_ORDER_BY = Contacts.STARRED + " DESC, " 2174e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar + TIMES_USED_SORT_COLUMN + " DESC, " 2184e815f8a8cae6c846cdca52420046cab902865deDaniel Dunbar + Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; 2199643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final String STREQUENT_LIMIT = 2209643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar "(SELECT COUNT(1) FROM " + Tables.CONTACTS + " WHERE " 2219643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar + Contacts.STARRED + "=1) + 25"; 2229643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar 223fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar private static final String FREQUENT_ORDER_BY = DataUsageStatColumns.TIMES_USED + " DESC," 22428c251b54b0b311749f07babe0f6909e71e877bcDaniel Dunbar + Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; 225fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar 226fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar /* package */ static final String UPDATE_TIMES_CONTACTED_CONTACTS_TABLE = 22728c251b54b0b311749f07babe0f6909e71e877bcDaniel Dunbar "UPDATE " + Tables.CONTACTS + " SET " + Contacts.TIMES_CONTACTED + "=" + 22828c251b54b0b311749f07babe0f6909e71e877bcDaniel Dunbar " CASE WHEN " + Contacts.TIMES_CONTACTED + " IS NULL THEN 1 ELSE " + 229fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar " (" + Contacts.TIMES_CONTACTED + " + 1) END WHERE " + Contacts._ID + "=?"; 230fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar 2311aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbar /* package */ static final String UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE = 2321aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbar "UPDATE " + Tables.RAW_CONTACTS + " SET " + RawContacts.TIMES_CONTACTED + "=" + 2331aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbar " CASE WHEN " + RawContacts.TIMES_CONTACTED + " IS NULL THEN 1 ELSE " + 2341aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbar " (" + RawContacts.TIMES_CONTACTED + " + 1) END WHERE " + RawContacts.CONTACT_ID + "=?"; 235fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar 236fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar /* package */ static final String PHONEBOOK_COLLATOR_NAME = "PHONEBOOK"; 237fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar 238fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar // Regex for splitting query strings - we split on any group of non-alphanumeric characters, 2399643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar // excluding the @ symbol. 240fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar /* package */ static final String QUERY_TOKENIZER_REGEX = "[^\\w@]+"; 2419643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar 24228c251b54b0b311749f07babe0f6909e71e877bcDaniel Dunbar private static final int CONTACTS = 1000; 2439643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int CONTACTS_ID = 1001; 2449643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int CONTACTS_LOOKUP = 1002; 2459643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int CONTACTS_LOOKUP_ID = 1003; 2469643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int CONTACTS_ID_DATA = 1004; 2479643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int CONTACTS_FILTER = 1005; 2489643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int CONTACTS_STREQUENT = 1006; 2499643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int CONTACTS_STREQUENT_FILTER = 1007; 2509643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int CONTACTS_GROUP = 1008; 2519643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int CONTACTS_ID_PHOTO = 1009; 2529643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int CONTACTS_ID_DISPLAY_PHOTO = 1010; 2539643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int CONTACTS_LOOKUP_DISPLAY_PHOTO = 1011; 2549643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int CONTACTS_LOOKUP_ID_DISPLAY_PHOTO = 1012; 2559643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int CONTACTS_AS_VCARD = 1013; 2569643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int CONTACTS_AS_MULTI_VCARD = 1014; 2579643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int CONTACTS_LOOKUP_DATA = 1015; 258fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar private static final int CONTACTS_LOOKUP_ID_DATA = 1016; 2599643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int CONTACTS_ID_ENTITIES = 1017; 2609643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int CONTACTS_LOOKUP_ENTITIES = 1018; 2619643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int CONTACTS_LOOKUP_ID_ENTITIES = 1019; 2629643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int CONTACTS_ID_STREAM_ITEMS = 1020; 2639643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int CONTACTS_LOOKUP_STREAM_ITEMS = 1021; 2649643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int CONTACTS_LOOKUP_ID_STREAM_ITEMS = 1022; 2659643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int CONTACTS_FREQUENT = 1023; 266fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar 2679643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int RAW_CONTACTS = 2002; 268fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar private static final int RAW_CONTACTS_ID = 2003; 269fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar private static final int RAW_CONTACTS_DATA = 2004; 2709643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int RAW_CONTACT_ENTITY_ID = 2005; 2719643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int RAW_CONTACTS_ID_DISPLAY_PHOTO = 2006; 272fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar private static final int RAW_CONTACTS_ID_STREAM_ITEMS = 2007; 2739643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar 2749643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int DATA = 3000; 2759643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int DATA_ID = 3001; 2769643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int PHONES = 3002; 277fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar private static final int PHONES_ID = 3003; 27828c251b54b0b311749f07babe0f6909e71e877bcDaniel Dunbar private static final int PHONES_FILTER = 3004; 279fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar private static final int EMAILS = 3005; 280fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar private static final int EMAILS_ID = 3006; 28128c251b54b0b311749f07babe0f6909e71e877bcDaniel Dunbar private static final int EMAILS_LOOKUP = 3007; 28228c251b54b0b311749f07babe0f6909e71e877bcDaniel Dunbar private static final int EMAILS_FILTER = 3008; 283fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar private static final int POSTALS = 3009; 284fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar private static final int POSTALS_ID = 3010; 2851aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbar 2861aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbar private static final int PHONE_LOOKUP = 4000; 287036c130e90eb5c93b0dc0a70ad07b9343623c2a8Dan Gohman 2881aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbar private static final int AGGREGATION_EXCEPTIONS = 6000; 289036c130e90eb5c93b0dc0a70ad07b9343623c2a8Dan Gohman private static final int AGGREGATION_EXCEPTION_ID = 6001; 290036c130e90eb5c93b0dc0a70ad07b9343623c2a8Dan Gohman 291036c130e90eb5c93b0dc0a70ad07b9343623c2a8Dan Gohman private static final int STATUS_UPDATES = 7000; 292036c130e90eb5c93b0dc0a70ad07b9343623c2a8Dan Gohman private static final int STATUS_UPDATES_ID = 7001; 2931aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbar 2941aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbar private static final int AGGREGATION_SUGGESTIONS = 8000; 295036c130e90eb5c93b0dc0a70ad07b9343623c2a8Dan Gohman 296036c130e90eb5c93b0dc0a70ad07b9343623c2a8Dan Gohman private static final int SETTINGS = 9000; 297036c130e90eb5c93b0dc0a70ad07b9343623c2a8Dan Gohman 298036c130e90eb5c93b0dc0a70ad07b9343623c2a8Dan Gohman private static final int GROUPS = 10000; 299036c130e90eb5c93b0dc0a70ad07b9343623c2a8Dan Gohman private static final int GROUPS_ID = 10001; 3001aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbar private static final int GROUPS_SUMMARY = 10003; 3011aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbar 3021aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbar private static final int SYNCSTATE = 11000; 303036c130e90eb5c93b0dc0a70ad07b9343623c2a8Dan Gohman private static final int SYNCSTATE_ID = 11001; 304036c130e90eb5c93b0dc0a70ad07b9343623c2a8Dan Gohman 3051aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbar private static final int SEARCH_SUGGESTIONS = 12001; 3061aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbar private static final int SEARCH_SHORTCUT = 12002; 307fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar 308fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar private static final int LIVE_FOLDERS_CONTACTS = 14000; 309fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar private static final int LIVE_FOLDERS_CONTACTS_WITH_PHONES = 14001; 310fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar private static final int LIVE_FOLDERS_CONTACTS_FAVORITES = 14002; 3119643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int LIVE_FOLDERS_CONTACTS_GROUP_NAME = 14003; 312fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar 3139643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int RAW_CONTACT_ENTITIES = 15001; 31428c251b54b0b311749f07babe0f6909e71e877bcDaniel Dunbar 3159643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int PROVIDER_STATUS = 16001; 3169643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar 3179643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int DIRECTORIES = 17001; 3189643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int DIRECTORIES_ID = 17002; 3199643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar 3209643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int COMPLETE_NAME = 18000; 3219643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar 3229643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int PROFILE = 19000; 3239643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int PROFILE_ENTITIES = 19001; 3249643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int PROFILE_DATA = 19002; 3259643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int PROFILE_DATA_ID = 19003; 3269643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int PROFILE_AS_VCARD = 19004; 3279643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int PROFILE_RAW_CONTACTS = 19005; 3289643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int PROFILE_RAW_CONTACTS_ID = 19006; 3299643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int PROFILE_RAW_CONTACTS_ID_DATA = 19007; 3309643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int PROFILE_RAW_CONTACTS_ID_ENTITIES = 19008; 3319643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar 3329643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int DATA_USAGE_FEEDBACK_ID = 20001; 3339643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar 3349643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int STREAM_ITEMS = 21000; 3359643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int STREAM_ITEMS_PHOTOS = 21001; 3369643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int STREAM_ITEMS_ID = 21002; 3379643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int STREAM_ITEMS_ID_PHOTOS = 21003; 3389643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int STREAM_ITEMS_ID_PHOTOS_ID = 21004; 3399643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int STREAM_ITEMS_LIMIT = 21005; 3409643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar 3419643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int DISPLAY_PHOTO = 22000; 3429643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final int PHOTO_DIMENSIONS = 22001; 3439643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar 3449643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final String SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID = 3459643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar RawContactsColumns.CONCRETE_ID + "=? AND " 3469643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar + GroupsColumns.CONCRETE_ACCOUNT_NAME 3479643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar + "=" + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AND " 3489643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar + GroupsColumns.CONCRETE_ACCOUNT_TYPE 349fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar + "=" + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " AND (" 3509643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar + GroupsColumns.CONCRETE_DATA_SET 3519643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar + "=" + RawContactsColumns.CONCRETE_DATA_SET + " OR " 3529643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar + GroupsColumns.CONCRETE_DATA_SET + " IS NULL AND " 3539643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar + RawContactsColumns.CONCRETE_DATA_SET + " IS NULL)" 3549643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar + " AND " + Groups.FAVORITES + " != 0"; 3559643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar 3569643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final String SELECTION_AUTO_ADD_GROUPS_BY_RAW_CONTACT_ID = 3579643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar RawContactsColumns.CONCRETE_ID + "=? AND " 3589643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar + GroupsColumns.CONCRETE_ACCOUNT_NAME + "=" 3599643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AND " 3609643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar + GroupsColumns.CONCRETE_ACCOUNT_TYPE + "=" 3619643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " AND (" 3629643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar + GroupsColumns.CONCRETE_DATA_SET + "=" 3639643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar + RawContactsColumns.CONCRETE_DATA_SET + " OR " 3649643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar + GroupsColumns.CONCRETE_DATA_SET + " IS NULL AND " 3659643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar + RawContactsColumns.CONCRETE_DATA_SET + " IS NULL)" 3669643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar + " AND " + Groups.AUTO_ADD + " != 0"; 3679643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar 3689643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final String[] PROJECTION_GROUP_ID 3699643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar = new String[]{Tables.GROUPS + "." + Groups._ID}; 3709643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar 3719643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final String SELECTION_GROUPMEMBERSHIP_DATA = DataColumns.MIMETYPE_ID + "=? " 3729643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar + "AND " + GroupMembership.GROUP_ROW_ID + "=? " 3739643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar + "AND " + GroupMembership.RAW_CONTACT_ID + "=?"; 3749643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar 3759643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private static final String SELECTION_STARRED_FROM_RAW_CONTACTS = 3769643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar "SELECT " + RawContacts.STARRED 3779643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar + " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + "=?"; 3789643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar 3799643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar public class AddressBookCursor extends CursorWrapper implements CrossProcessCursor { 3809643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private final CrossProcessCursor mCursor; 3819643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar private final Bundle mBundle; 3829643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar 3839643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar public AddressBookCursor(CrossProcessCursor cursor, String[] titles, int[] counts) { 3849643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar super(cursor); 3859643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar mCursor = cursor; 3869643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar mBundle = new Bundle(); 3879643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar mBundle.putStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES, titles); 3889643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar mBundle.putIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS, counts); 3899643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar } 3909643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar 3919643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar @Override 3929643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar public Bundle getExtras() { 3939643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar return mBundle; 3949643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar } 3959643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar 3969643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar @Override 3979643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar public void fillWindow(int pos, CursorWindow window) { 398fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar mCursor.fillWindow(pos, window); 3999643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar } 400fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar 401fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar @Override 4021aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbar public CursorWindow getWindow() { 4039643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar return mCursor.getWindow(); 4041aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbar } 4051aa14aac4198bca0f44e4adad42bf6238cbf9757Daniel Dunbar 4069643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar @Override 4079643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar public boolean onMove(int oldPosition, int newPosition) { 4089643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar return mCursor.onMove(oldPosition, newPosition); 409fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar } 4109643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar } 4119643ac55142d40da404caa8e5fedfef2cd7b4afcDaniel Dunbar 412fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar private interface DataContactsQuery { 41328c251b54b0b311749f07babe0f6909e71e877bcDaniel Dunbar public static final String TABLE = "data " 414fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) " 415fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar + "JOIN contacts ON (raw_contacts.contact_id = contacts._id)"; 4165d917a8952c09a345180ec36f0df4ee5dd5eddeaChris Lattner 4175d917a8952c09a345180ec36f0df4ee5dd5eddeaChris Lattner public static final String[] PROJECTION = new String[] { 4185d917a8952c09a345180ec36f0df4ee5dd5eddeaChris Lattner RawContactsColumns.CONCRETE_ID, 4195d917a8952c09a345180ec36f0df4ee5dd5eddeaChris Lattner RawContactsColumns.CONCRETE_ACCOUNT_TYPE, 4205d917a8952c09a345180ec36f0df4ee5dd5eddeaChris Lattner RawContactsColumns.CONCRETE_ACCOUNT_NAME, 4215d917a8952c09a345180ec36f0df4ee5dd5eddeaChris Lattner RawContactsColumns.CONCRETE_DATA_SET, 422df9c4380ee7e60c1de5cae32685b113170b1faa2Chris Lattner DataColumns.CONCRETE_ID, 4235d917a8952c09a345180ec36f0df4ee5dd5eddeaChris Lattner ContactsColumns.CONCRETE_ID 4245d917a8952c09a345180ec36f0df4ee5dd5eddeaChris Lattner }; 425e3e7a369f20af66a96830d8bfe52668c9e2e1fa1Chris Lattner 4265d917a8952c09a345180ec36f0df4ee5dd5eddeaChris Lattner public static final int RAW_CONTACT_ID = 0; 427a0e36d55c495b3325805c659ac365b5faea84e34Daniel Dunbar public static final int ACCOUNT_TYPE = 1; 4285d917a8952c09a345180ec36f0df4ee5dd5eddeaChris Lattner public static final int ACCOUNT_NAME = 2; 429f82f4490b130eca55b08d605456a4ceacccf288aDaniel Dunbar public static final int DATA_SET = 3; 430a0e36d55c495b3325805c659ac365b5faea84e34Daniel Dunbar public static final int DATA_ID = 4; 4317597212abced110723f2fee985a7d60557c092ecEvan Cheng public static final int CONTACT_ID = 5; 4320eab5c4d85b4c4bb161bcdd959aa58a6f54415ccDaniel Dunbar } 4335d917a8952c09a345180ec36f0df4ee5dd5eddeaChris Lattner 4345d917a8952c09a345180ec36f0df4ee5dd5eddeaChris Lattner interface RawContactsQuery { 4355d917a8952c09a345180ec36f0df4ee5dd5eddeaChris Lattner String TABLE = Tables.RAW_CONTACTS; 4365d917a8952c09a345180ec36f0df4ee5dd5eddeaChris Lattner 4375d917a8952c09a345180ec36f0df4ee5dd5eddeaChris Lattner String[] COLUMNS = new String[] { 4385d917a8952c09a345180ec36f0df4ee5dd5eddeaChris Lattner RawContacts.DELETED, 4395d917a8952c09a345180ec36f0df4ee5dd5eddeaChris Lattner RawContacts.ACCOUNT_TYPE, 440fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar RawContacts.ACCOUNT_NAME, 441fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar RawContacts.DATA_SET, 442fc6877aec9826fa830204d49eba7fac7412b841eDaniel Dunbar }; 443 444 int DELETED = 0; 445 int ACCOUNT_TYPE = 1; 446 int ACCOUNT_NAME = 2; 447 int DATA_SET = 3; 448 } 449 450 public static final String DEFAULT_ACCOUNT_TYPE = "com.google"; 451 452 /** Sql where statement for filtering on groups. */ 453 private static final String CONTACTS_IN_GROUP_SELECT = 454 Contacts._ID + " IN " 455 + "(SELECT " + RawContacts.CONTACT_ID 456 + " FROM " + Tables.RAW_CONTACTS 457 + " WHERE " + RawContactsColumns.CONCRETE_ID + " IN " 458 + "(SELECT " + DataColumns.CONCRETE_RAW_CONTACT_ID 459 + " FROM " + Tables.DATA_JOIN_MIMETYPES 460 + " WHERE " + Data.MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE 461 + "' AND " + GroupMembership.GROUP_ROW_ID + "=" 462 + "(SELECT " + Tables.GROUPS + "." + Groups._ID 463 + " FROM " + Tables.GROUPS 464 + " WHERE " + Groups.TITLE + "=?)))"; 465 466 /** Sql for updating DIRTY flag on multiple raw contacts */ 467 private static final String UPDATE_RAW_CONTACT_SET_DIRTY_SQL = 468 "UPDATE " + Tables.RAW_CONTACTS + 469 " SET " + RawContacts.DIRTY + "=1" + 470 " WHERE " + RawContacts._ID + " IN ("; 471 472 /** Sql for updating VERSION on multiple raw contacts */ 473 private static final String UPDATE_RAW_CONTACT_SET_VERSION_SQL = 474 "UPDATE " + Tables.RAW_CONTACTS + 475 " SET " + RawContacts.VERSION + " = " + RawContacts.VERSION + " + 1" + 476 " WHERE " + RawContacts._ID + " IN ("; 477 478 // Current contacts - those contacted within the last 3 days (in seconds) 479 private static final long EMAIL_FILTER_CURRENT = 3 * 24 * 60 * 60; 480 481 // Recent contacts - those contacted within the last 30 days (in seconds) 482 private static final long EMAIL_FILTER_RECENT = 30 * 24 * 60 * 60; 483 484 private static final String TIME_SINCE_LAST_USED = 485 "(strftime('%s', 'now') - " + DataUsageStatColumns.LAST_TIME_USED + "/1000)"; 486 487 /* 488 * Sorting order for email address suggestions: first starred, then the rest. 489 * second in_visible_group, then the rest. 490 * Within the four (starred/unstarred, in_visible_group/not-in_visible_group) groups 491 * - three buckets: very recently contacted, then fairly 492 * recently contacted, then the rest. Within each of the bucket - descending count 493 * of times contacted (both for data row and for contact row). If all else fails, alphabetical. 494 * (Super)primary email address is returned before other addresses for the same contact. 495 */ 496 private static final String EMAIL_FILTER_SORT_ORDER = 497 Contacts.STARRED + " DESC, " 498 + Contacts.IN_VISIBLE_GROUP + " DESC, " 499 + "(CASE WHEN " + TIME_SINCE_LAST_USED + " < " + EMAIL_FILTER_CURRENT 500 + " THEN 0 " 501 + " WHEN " + TIME_SINCE_LAST_USED + " < " + EMAIL_FILTER_RECENT 502 + " THEN 1 " 503 + " ELSE 2 END), " 504 + DataUsageStatColumns.TIMES_USED + " DESC, " 505 + Contacts.DISPLAY_NAME + ", " 506 + Data.CONTACT_ID + ", " 507 + Data.IS_SUPER_PRIMARY + " DESC, " 508 + Data.IS_PRIMARY + " DESC"; 509 510 /** Currently same as {@link #EMAIL_FILTER_SORT_ORDER} */ 511 private static final String PHONE_FILTER_SORT_ORDER = EMAIL_FILTER_SORT_ORDER; 512 513 /** Name lookup types used for contact filtering */ 514 private static final String CONTACT_LOOKUP_NAME_TYPES = 515 NameLookupType.NAME_COLLATION_KEY + "," + 516 NameLookupType.EMAIL_BASED_NICKNAME + "," + 517 NameLookupType.NICKNAME; 518 519 /** 520 * If any of these columns are used in a Data projection, there is no point in 521 * using the DISTINCT keyword, which can negatively affect performance. 522 */ 523 private static final String[] DISTINCT_DATA_PROHIBITING_COLUMNS = { 524 Data._ID, 525 Data.RAW_CONTACT_ID, 526 Data.NAME_RAW_CONTACT_ID, 527 RawContacts.ACCOUNT_NAME, 528 RawContacts.ACCOUNT_TYPE, 529 RawContacts.DATA_SET, 530 RawContacts.ACCOUNT_TYPE_AND_DATA_SET, 531 RawContacts.DIRTY, 532 RawContacts.NAME_VERIFIED, 533 RawContacts.SOURCE_ID, 534 RawContacts.VERSION, 535 }; 536 537 private static final ProjectionMap sContactsColumns = ProjectionMap.builder() 538 .add(Contacts.CUSTOM_RINGTONE) 539 .add(Contacts.DISPLAY_NAME) 540 .add(Contacts.DISPLAY_NAME_ALTERNATIVE) 541 .add(Contacts.DISPLAY_NAME_SOURCE) 542 .add(Contacts.IN_VISIBLE_GROUP) 543 .add(Contacts.LAST_TIME_CONTACTED) 544 .add(Contacts.LOOKUP_KEY) 545 .add(Contacts.PHONETIC_NAME) 546 .add(Contacts.PHONETIC_NAME_STYLE) 547 .add(Contacts.PHOTO_ID) 548 .add(Contacts.PHOTO_FILE_ID) 549 .add(Contacts.PHOTO_URI) 550 .add(Contacts.PHOTO_THUMBNAIL_URI) 551 .add(Contacts.SEND_TO_VOICEMAIL) 552 .add(Contacts.SORT_KEY_ALTERNATIVE) 553 .add(Contacts.SORT_KEY_PRIMARY) 554 .add(Contacts.STARRED) 555 .add(Contacts.TIMES_CONTACTED) 556 .add(Contacts.HAS_PHONE_NUMBER) 557 .build(); 558 559 private static final ProjectionMap sContactsPresenceColumns = ProjectionMap.builder() 560 .add(Contacts.CONTACT_PRESENCE, 561 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE) 562 .add(Contacts.CONTACT_CHAT_CAPABILITY, 563 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY) 564 .add(Contacts.CONTACT_STATUS, 565 ContactsStatusUpdatesColumns.CONCRETE_STATUS) 566 .add(Contacts.CONTACT_STATUS_TIMESTAMP, 567 ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP) 568 .add(Contacts.CONTACT_STATUS_RES_PACKAGE, 569 ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE) 570 .add(Contacts.CONTACT_STATUS_LABEL, 571 ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL) 572 .add(Contacts.CONTACT_STATUS_ICON, 573 ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON) 574 .build(); 575 576 private static final ProjectionMap sSnippetColumns = ProjectionMap.builder() 577 .add(SearchSnippetColumns.SNIPPET) 578 .build(); 579 580 private static final ProjectionMap sRawContactColumns = ProjectionMap.builder() 581 .add(RawContacts.ACCOUNT_NAME) 582 .add(RawContacts.ACCOUNT_TYPE) 583 .add(RawContacts.DATA_SET) 584 .add(RawContacts.ACCOUNT_TYPE_AND_DATA_SET) 585 .add(RawContacts.DIRTY) 586 .add(RawContacts.NAME_VERIFIED) 587 .add(RawContacts.SOURCE_ID) 588 .add(RawContacts.VERSION) 589 .build(); 590 591 private static final ProjectionMap sRawContactSyncColumns = ProjectionMap.builder() 592 .add(RawContacts.SYNC1) 593 .add(RawContacts.SYNC2) 594 .add(RawContacts.SYNC3) 595 .add(RawContacts.SYNC4) 596 .build(); 597 598 private static final ProjectionMap sDataColumns = ProjectionMap.builder() 599 .add(Data.DATA1) 600 .add(Data.DATA2) 601 .add(Data.DATA3) 602 .add(Data.DATA4) 603 .add(Data.DATA5) 604 .add(Data.DATA6) 605 .add(Data.DATA7) 606 .add(Data.DATA8) 607 .add(Data.DATA9) 608 .add(Data.DATA10) 609 .add(Data.DATA11) 610 .add(Data.DATA12) 611 .add(Data.DATA13) 612 .add(Data.DATA14) 613 .add(Data.DATA15) 614 .add(Data.DATA_VERSION) 615 .add(Data.IS_PRIMARY) 616 .add(Data.IS_SUPER_PRIMARY) 617 .add(Data.MIMETYPE) 618 .add(Data.RES_PACKAGE) 619 .add(Data.SYNC1) 620 .add(Data.SYNC2) 621 .add(Data.SYNC3) 622 .add(Data.SYNC4) 623 .add(GroupMembership.GROUP_SOURCE_ID) 624 .build(); 625 626 private static final ProjectionMap sContactPresenceColumns = ProjectionMap.builder() 627 .add(Contacts.CONTACT_PRESENCE, 628 Tables.AGGREGATED_PRESENCE + '.' + StatusUpdates.PRESENCE) 629 .add(Contacts.CONTACT_CHAT_CAPABILITY, 630 Tables.AGGREGATED_PRESENCE + '.' + StatusUpdates.CHAT_CAPABILITY) 631 .add(Contacts.CONTACT_STATUS, 632 ContactsStatusUpdatesColumns.CONCRETE_STATUS) 633 .add(Contacts.CONTACT_STATUS_TIMESTAMP, 634 ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP) 635 .add(Contacts.CONTACT_STATUS_RES_PACKAGE, 636 ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE) 637 .add(Contacts.CONTACT_STATUS_LABEL, 638 ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL) 639 .add(Contacts.CONTACT_STATUS_ICON, 640 ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON) 641 .build(); 642 643 private static final ProjectionMap sDataPresenceColumns = ProjectionMap.builder() 644 .add(Data.PRESENCE, Tables.PRESENCE + "." + StatusUpdates.PRESENCE) 645 .add(Data.CHAT_CAPABILITY, Tables.PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY) 646 .add(Data.STATUS, StatusUpdatesColumns.CONCRETE_STATUS) 647 .add(Data.STATUS_TIMESTAMP, StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP) 648 .add(Data.STATUS_RES_PACKAGE, StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE) 649 .add(Data.STATUS_LABEL, StatusUpdatesColumns.CONCRETE_STATUS_LABEL) 650 .add(Data.STATUS_ICON, StatusUpdatesColumns.CONCRETE_STATUS_ICON) 651 .build(); 652 653 /** Contains just BaseColumns._COUNT */ 654 private static final ProjectionMap sCountProjectionMap = ProjectionMap.builder() 655 .add(BaseColumns._COUNT, "COUNT(*)") 656 .build(); 657 658 /** Contains just the contacts columns */ 659 private static final ProjectionMap sContactsProjectionMap = ProjectionMap.builder() 660 .add(Contacts._ID) 661 .add(Contacts.HAS_PHONE_NUMBER) 662 .add(Contacts.NAME_RAW_CONTACT_ID) 663 .add(Contacts.IS_USER_PROFILE) 664 .addAll(sContactsColumns) 665 .addAll(sContactsPresenceColumns) 666 .build(); 667 668 /** Contains just the contacts columns */ 669 private static final ProjectionMap sContactsProjectionWithSnippetMap = ProjectionMap.builder() 670 .addAll(sContactsProjectionMap) 671 .addAll(sSnippetColumns) 672 .build(); 673 674 /** Used for pushing starred contacts to the top of a times contacted list **/ 675 private static final ProjectionMap sStrequentStarredProjectionMap = ProjectionMap.builder() 676 .addAll(sContactsProjectionMap) 677 .add(TIMES_USED_SORT_COLUMN, String.valueOf(Long.MAX_VALUE)) 678 .build(); 679 680 private static final ProjectionMap sStrequentFrequentProjectionMap = ProjectionMap.builder() 681 .addAll(sContactsProjectionMap) 682 .add(TIMES_USED_SORT_COLUMN, "SUM(" + DataUsageStatColumns.CONCRETE_TIMES_USED + ")") 683 .build(); 684 685 /** 686 * Used for Strequent Uri with {@link ContactsContract#STREQUENT_PHONE_ONLY}, which allows 687 * users to obtain part of Data columns. Right now Starred part just returns NULL for 688 * those data columns (frequent part should return real ones in data table). 689 **/ 690 private static final ProjectionMap sStrequentPhoneOnlyStarredProjectionMap 691 = ProjectionMap.builder() 692 .addAll(sContactsProjectionMap) 693 .add(TIMES_USED_SORT_COLUMN, String.valueOf(Long.MAX_VALUE)) 694 .add(Phone.NUMBER, "NULL") 695 .add(Phone.TYPE, "NULL") 696 .add(Phone.LABEL, "NULL") 697 .build(); 698 699 /** 700 * Used for Strequent Uri with {@link ContactsContract#STREQUENT_PHONE_ONLY}, which allows 701 * users to obtain part of Data columns. We hard-code {@link Contacts#IS_USER_PROFILE} to NULL, 702 * because sContactsProjectionMap specifies a field that doesn't exist in the view behind the 703 * query that uses this projection map. 704 **/ 705 private static final ProjectionMap sStrequentPhoneOnlyFrequentProjectionMap 706 = ProjectionMap.builder() 707 .addAll(sContactsProjectionMap) 708 .add(TIMES_USED_SORT_COLUMN, DataUsageStatColumns.CONCRETE_TIMES_USED) 709 .add(Phone.NUMBER) 710 .add(Phone.TYPE) 711 .add(Phone.LABEL) 712 .add(Contacts.IS_USER_PROFILE, "NULL") 713 .build(); 714 715 /** Contains just the contacts vCard columns */ 716 private static final ProjectionMap sContactsVCardProjectionMap = ProjectionMap.builder() 717 .add(Contacts._ID) 718 .add(OpenableColumns.DISPLAY_NAME, Contacts.DISPLAY_NAME + " || '.vcf'") 719 .add(OpenableColumns.SIZE, "NULL") 720 .build(); 721 722 /** Contains just the raw contacts columns */ 723 private static final ProjectionMap sRawContactsProjectionMap = ProjectionMap.builder() 724 .add(RawContacts._ID) 725 .add(RawContacts.CONTACT_ID) 726 .add(RawContacts.DELETED) 727 .add(RawContacts.DISPLAY_NAME_PRIMARY) 728 .add(RawContacts.DISPLAY_NAME_ALTERNATIVE) 729 .add(RawContacts.DISPLAY_NAME_SOURCE) 730 .add(RawContacts.PHONETIC_NAME) 731 .add(RawContacts.PHONETIC_NAME_STYLE) 732 .add(RawContacts.SORT_KEY_PRIMARY) 733 .add(RawContacts.SORT_KEY_ALTERNATIVE) 734 .add(RawContacts.TIMES_CONTACTED) 735 .add(RawContacts.LAST_TIME_CONTACTED) 736 .add(RawContacts.CUSTOM_RINGTONE) 737 .add(RawContacts.SEND_TO_VOICEMAIL) 738 .add(RawContacts.STARRED) 739 .add(RawContacts.AGGREGATION_MODE) 740 .add(RawContacts.RAW_CONTACT_IS_USER_PROFILE) 741 .addAll(sRawContactColumns) 742 .addAll(sRawContactSyncColumns) 743 .build(); 744 745 /** Contains the columns from the raw entity view*/ 746 private static final ProjectionMap sRawEntityProjectionMap = ProjectionMap.builder() 747 .add(RawContacts._ID) 748 .add(RawContacts.CONTACT_ID) 749 .add(RawContacts.Entity.DATA_ID) 750 .add(RawContacts.DELETED) 751 .add(RawContacts.STARRED) 752 .add(RawContacts.RAW_CONTACT_IS_USER_PROFILE) 753 .addAll(sRawContactColumns) 754 .addAll(sRawContactSyncColumns) 755 .addAll(sDataColumns) 756 .build(); 757 758 /** Contains the columns from the contact entity view*/ 759 private static final ProjectionMap sEntityProjectionMap = ProjectionMap.builder() 760 .add(Contacts.Entity._ID) 761 .add(Contacts.Entity.CONTACT_ID) 762 .add(Contacts.Entity.RAW_CONTACT_ID) 763 .add(Contacts.Entity.DATA_ID) 764 .add(Contacts.Entity.NAME_RAW_CONTACT_ID) 765 .add(Contacts.Entity.DELETED) 766 .add(Contacts.IS_USER_PROFILE) 767 .addAll(sContactsColumns) 768 .addAll(sContactPresenceColumns) 769 .addAll(sRawContactColumns) 770 .addAll(sRawContactSyncColumns) 771 .addAll(sDataColumns) 772 .addAll(sDataPresenceColumns) 773 .build(); 774 775 /** Contains columns from the data view */ 776 private static final ProjectionMap sDataProjectionMap = ProjectionMap.builder() 777 .add(Data._ID) 778 .add(Data.RAW_CONTACT_ID) 779 .add(Data.CONTACT_ID) 780 .add(Data.NAME_RAW_CONTACT_ID) 781 .add(RawContacts.RAW_CONTACT_IS_USER_PROFILE) 782 .addAll(sDataColumns) 783 .addAll(sDataPresenceColumns) 784 .addAll(sRawContactColumns) 785 .addAll(sContactsColumns) 786 .addAll(sContactPresenceColumns) 787 .build(); 788 789 /** Contains columns from the data view */ 790 private static final ProjectionMap sDistinctDataProjectionMap = ProjectionMap.builder() 791 .add(Data._ID, "MIN(" + Data._ID + ")") 792 .add(RawContacts.CONTACT_ID) 793 .add(RawContacts.RAW_CONTACT_IS_USER_PROFILE) 794 .addAll(sDataColumns) 795 .addAll(sDataPresenceColumns) 796 .addAll(sContactsColumns) 797 .addAll(sContactPresenceColumns) 798 .build(); 799 800 /** Contains the data and contacts columns, for joined tables */ 801 private static final ProjectionMap sPhoneLookupProjectionMap = ProjectionMap.builder() 802 .add(PhoneLookup._ID, "contacts_view." + Contacts._ID) 803 .add(PhoneLookup.LOOKUP_KEY, "contacts_view." + Contacts.LOOKUP_KEY) 804 .add(PhoneLookup.DISPLAY_NAME, "contacts_view." + Contacts.DISPLAY_NAME) 805 .add(PhoneLookup.LAST_TIME_CONTACTED, "contacts_view." + Contacts.LAST_TIME_CONTACTED) 806 .add(PhoneLookup.TIMES_CONTACTED, "contacts_view." + Contacts.TIMES_CONTACTED) 807 .add(PhoneLookup.STARRED, "contacts_view." + Contacts.STARRED) 808 .add(PhoneLookup.IN_VISIBLE_GROUP, "contacts_view." + Contacts.IN_VISIBLE_GROUP) 809 .add(PhoneLookup.PHOTO_ID, "contacts_view." + Contacts.PHOTO_ID) 810 .add(PhoneLookup.PHOTO_URI, "contacts_view." + Contacts.PHOTO_URI) 811 .add(PhoneLookup.PHOTO_THUMBNAIL_URI, "contacts_view." + Contacts.PHOTO_THUMBNAIL_URI) 812 .add(PhoneLookup.CUSTOM_RINGTONE, "contacts_view." + Contacts.CUSTOM_RINGTONE) 813 .add(PhoneLookup.HAS_PHONE_NUMBER, "contacts_view." + Contacts.HAS_PHONE_NUMBER) 814 .add(PhoneLookup.SEND_TO_VOICEMAIL, "contacts_view." + Contacts.SEND_TO_VOICEMAIL) 815 .add(PhoneLookup.NUMBER, Phone.NUMBER) 816 .add(PhoneLookup.TYPE, Phone.TYPE) 817 .add(PhoneLookup.LABEL, Phone.LABEL) 818 .add(PhoneLookup.NORMALIZED_NUMBER, Phone.NORMALIZED_NUMBER) 819 .build(); 820 821 /** Contains the just the {@link Groups} columns */ 822 private static final ProjectionMap sGroupsProjectionMap = ProjectionMap.builder() 823 .add(Groups._ID) 824 .add(Groups.ACCOUNT_NAME) 825 .add(Groups.ACCOUNT_TYPE) 826 .add(Groups.DATA_SET) 827 .add(Groups.ACCOUNT_TYPE_AND_DATA_SET) 828 .add(Groups.SOURCE_ID) 829 .add(Groups.DIRTY) 830 .add(Groups.VERSION) 831 .add(Groups.RES_PACKAGE) 832 .add(Groups.TITLE) 833 .add(Groups.TITLE_RES) 834 .add(Groups.GROUP_VISIBLE) 835 .add(Groups.SYSTEM_ID) 836 .add(Groups.DELETED) 837 .add(Groups.NOTES) 838 .add(Groups.ACTION) 839 .add(Groups.ACTION_URI) 840 .add(Groups.SHOULD_SYNC) 841 .add(Groups.FAVORITES) 842 .add(Groups.AUTO_ADD) 843 .add(Groups.GROUP_IS_READ_ONLY) 844 .add(Groups.SYNC1) 845 .add(Groups.SYNC2) 846 .add(Groups.SYNC3) 847 .add(Groups.SYNC4) 848 .build(); 849 850 /** Contains {@link Groups} columns along with summary details */ 851 private static final ProjectionMap sGroupsSummaryProjectionMap = ProjectionMap.builder() 852 .addAll(sGroupsProjectionMap) 853 .add(Groups.SUMMARY_COUNT, 854 "(SELECT COUNT(" + ContactsColumns.CONCRETE_ID + ") FROM " 855 + Tables.CONTACTS_JOIN_RAW_CONTACTS_DATA_FILTERED_BY_GROUPMEMBERSHIP 856 + ")") 857 .add(Groups.SUMMARY_WITH_PHONES, 858 "(SELECT COUNT(" + ContactsColumns.CONCRETE_ID + ") FROM " 859 + Tables.CONTACTS_JOIN_RAW_CONTACTS_DATA_FILTERED_BY_GROUPMEMBERSHIP 860 + " WHERE " + Contacts.HAS_PHONE_NUMBER + ")") 861 .build(); 862 863 // This is only exposed as hidden API for the contacts app, so we can be very specific in 864 // the filtering 865 private static final ProjectionMap sGroupsSummaryProjectionMapWithGroupCountPerAccount = 866 ProjectionMap.builder() 867 .addAll(sGroupsSummaryProjectionMap) 868 .add(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT, 869 "(SELECT COUNT(*) FROM " + Views.GROUPS + " WHERE " 870 + "(" + Groups.ACCOUNT_NAME + "=" 871 + GroupsColumns.CONCRETE_ACCOUNT_NAME 872 + " AND " 873 + Groups.ACCOUNT_TYPE + "=" + GroupsColumns.CONCRETE_ACCOUNT_TYPE 874 + " AND " 875 + Groups.DELETED + "=0 AND " 876 + Groups.FAVORITES + "=0 AND " 877 + Groups.AUTO_ADD + "=0" 878 + ")" 879 + " GROUP BY " 880 + Groups.ACCOUNT_NAME + ", " + Groups.ACCOUNT_TYPE 881 + ")") 882 .build(); 883 884 /** Contains the agg_exceptions columns */ 885 private static final ProjectionMap sAggregationExceptionsProjectionMap = ProjectionMap.builder() 886 .add(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id") 887 .add(AggregationExceptions.TYPE) 888 .add(AggregationExceptions.RAW_CONTACT_ID1) 889 .add(AggregationExceptions.RAW_CONTACT_ID2) 890 .build(); 891 892 /** Contains the agg_exceptions columns */ 893 private static final ProjectionMap sSettingsProjectionMap = ProjectionMap.builder() 894 .add(Settings.ACCOUNT_NAME) 895 .add(Settings.ACCOUNT_TYPE) 896 .add(Settings.UNGROUPED_VISIBLE) 897 .add(Settings.SHOULD_SYNC) 898 .add(Settings.ANY_UNSYNCED, 899 "(CASE WHEN MIN(" + Settings.SHOULD_SYNC 900 + ",(SELECT " 901 + "(CASE WHEN MIN(" + Groups.SHOULD_SYNC + ") IS NULL" 902 + " THEN 1" 903 + " ELSE MIN(" + Groups.SHOULD_SYNC + ")" 904 + " END)" 905 + " FROM " + Tables.GROUPS 906 + " WHERE " + GroupsColumns.CONCRETE_ACCOUNT_NAME + "=" 907 + SettingsColumns.CONCRETE_ACCOUNT_NAME 908 + " AND " + GroupsColumns.CONCRETE_ACCOUNT_TYPE + "=" 909 + SettingsColumns.CONCRETE_ACCOUNT_TYPE + "))=0" 910 + " THEN 1" 911 + " ELSE 0" 912 + " END)") 913 .add(Settings.UNGROUPED_COUNT, 914 "(SELECT COUNT(*)" 915 + " FROM (SELECT 1" 916 + " FROM " + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS 917 + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID 918 + " HAVING " + Clauses.HAVING_NO_GROUPS 919 + "))") 920 .add(Settings.UNGROUPED_WITH_PHONES, 921 "(SELECT COUNT(*)" 922 + " FROM (SELECT 1" 923 + " FROM " + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS 924 + " WHERE " + Contacts.HAS_PHONE_NUMBER 925 + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID 926 + " HAVING " + Clauses.HAVING_NO_GROUPS 927 + "))") 928 .build(); 929 930 /** Contains StatusUpdates columns */ 931 private static final ProjectionMap sStatusUpdatesProjectionMap = ProjectionMap.builder() 932 .add(PresenceColumns.RAW_CONTACT_ID) 933 .add(StatusUpdates.DATA_ID, DataColumns.CONCRETE_ID) 934 .add(StatusUpdates.IM_ACCOUNT) 935 .add(StatusUpdates.IM_HANDLE) 936 .add(StatusUpdates.PROTOCOL) 937 // We cannot allow a null in the custom protocol field, because SQLite3 does not 938 // properly enforce uniqueness of null values 939 .add(StatusUpdates.CUSTOM_PROTOCOL, 940 "(CASE WHEN " + StatusUpdates.CUSTOM_PROTOCOL + "=''" 941 + " THEN NULL" 942 + " ELSE " + StatusUpdates.CUSTOM_PROTOCOL + " END)") 943 .add(StatusUpdates.PRESENCE) 944 .add(StatusUpdates.CHAT_CAPABILITY) 945 .add(StatusUpdates.STATUS) 946 .add(StatusUpdates.STATUS_TIMESTAMP) 947 .add(StatusUpdates.STATUS_RES_PACKAGE) 948 .add(StatusUpdates.STATUS_ICON) 949 .add(StatusUpdates.STATUS_LABEL) 950 .build(); 951 952 /** Contains StreamItems columns */ 953 private static final ProjectionMap sStreamItemsProjectionMap = ProjectionMap.builder() 954 .add(StreamItems._ID, StreamItemsColumns.CONCRETE_ID) 955 .add(RawContacts.CONTACT_ID) 956 .add(StreamItems.RAW_CONTACT_ID) 957 .add(StreamItems.RES_PACKAGE) 958 .add(StreamItems.RES_ICON) 959 .add(StreamItems.RES_LABEL) 960 .add(StreamItems.TEXT) 961 .add(StreamItems.TIMESTAMP) 962 .add(StreamItems.COMMENTS) 963 .add(StreamItems.ACTION) 964 .add(StreamItems.ACTION_URI) 965 .build(); 966 967 private static final ProjectionMap sStreamItemPhotosProjectionMap = ProjectionMap.builder() 968 .add(StreamItemPhotos._ID, StreamItemPhotosColumns.CONCRETE_ID) 969 .add(StreamItems.RAW_CONTACT_ID) 970 .add(StreamItemPhotos.STREAM_ITEM_ID) 971 .add(StreamItemPhotos.SORT_INDEX) 972 .add(StreamItemPhotos.PHOTO_FILE_ID) 973 .add(StreamItemPhotos.PHOTO_URI, 974 "'" + DisplayPhoto.CONTENT_URI + "'||'/'||" + StreamItemPhotos.PHOTO_FILE_ID) 975 .add(StreamItemPhotos.ACTION, StreamItemPhotosColumns.CONCRETE_ACTION) 976 .add(StreamItemPhotos.ACTION_URI, StreamItemPhotosColumns.CONCRETE_ACTION_URI) 977 .add(PhotoFiles.HEIGHT) 978 .add(PhotoFiles.WIDTH) 979 .add(PhotoFiles.FILESIZE) 980 .build(); 981 982 /** Contains Live Folders columns */ 983 private static final ProjectionMap sLiveFoldersProjectionMap = ProjectionMap.builder() 984 .add(LiveFolders._ID, Contacts._ID) 985 .add(LiveFolders.NAME, Contacts.DISPLAY_NAME) 986 // TODO: Put contact photo back when we have a way to display a default icon 987 // for contacts without a photo 988 // .add(LiveFolders.ICON_BITMAP, Photos.DATA) 989 .build(); 990 991 /** Contains {@link Directory} columns */ 992 private static final ProjectionMap sDirectoryProjectionMap = ProjectionMap.builder() 993 .add(Directory._ID) 994 .add(Directory.PACKAGE_NAME) 995 .add(Directory.TYPE_RESOURCE_ID) 996 .add(Directory.DISPLAY_NAME) 997 .add(Directory.DIRECTORY_AUTHORITY) 998 .add(Directory.ACCOUNT_TYPE) 999 .add(Directory.ACCOUNT_NAME) 1000 .add(Directory.EXPORT_SUPPORT) 1001 .add(Directory.SHORTCUT_SUPPORT) 1002 .add(Directory.PHOTO_SUPPORT) 1003 .build(); 1004 1005 // where clause to update the status_updates table 1006 private static final String WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE = 1007 StatusUpdatesColumns.DATA_ID + " IN (SELECT Distinct " + StatusUpdates.DATA_ID + 1008 " FROM " + Tables.STATUS_UPDATES + " LEFT OUTER JOIN " + Tables.PRESENCE + 1009 " ON " + StatusUpdatesColumns.DATA_ID + " = " + StatusUpdates.DATA_ID + " WHERE "; 1010 1011 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 1012 1013 /** 1014 * Notification ID for failure to import contacts. 1015 */ 1016 private static final int LEGACY_IMPORT_FAILED_NOTIFICATION = 1; 1017 1018 private static final String DEFAULT_SNIPPET_ARG_START_MATCH = "["; 1019 private static final String DEFAULT_SNIPPET_ARG_END_MATCH = "]"; 1020 private static final String DEFAULT_SNIPPET_ARG_ELLIPSIS = "..."; 1021 private static final int DEFAULT_SNIPPET_ARG_MAX_TOKENS = -10; 1022 1023 private boolean sIsPhoneInitialized; 1024 private boolean sIsPhone; 1025 1026 private StringBuilder mSb = new StringBuilder(); 1027 private String[] mSelectionArgs1 = new String[1]; 1028 private String[] mSelectionArgs2 = new String[2]; 1029 private ArrayList<String> mSelectionArgs = Lists.newArrayList(); 1030 1031 private Account mAccount; 1032 1033 /** 1034 * Stores mapping from type Strings exposed via {@link DataUsageFeedback} to 1035 * type integers in {@link DataUsageStatColumns}. 1036 */ 1037 private static final Map<String, Integer> sDataUsageTypeMap; 1038 1039 static { 1040 // Contacts URI matching table 1041 final UriMatcher matcher = sUriMatcher; 1042 matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS); 1043 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID); 1044 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_ID_DATA); 1045 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/entities", CONTACTS_ID_ENTITIES); 1046 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions", 1047 AGGREGATION_SUGGESTIONS); 1048 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions/*", 1049 AGGREGATION_SUGGESTIONS); 1050 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", CONTACTS_ID_PHOTO); 1051 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/display_photo", 1052 CONTACTS_ID_DISPLAY_PHOTO); 1053 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/stream_items", 1054 CONTACTS_ID_STREAM_ITEMS); 1055 matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter", CONTACTS_FILTER); 1056 matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter/*", CONTACTS_FILTER); 1057 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", CONTACTS_LOOKUP); 1058 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/data", CONTACTS_LOOKUP_DATA); 1059 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", CONTACTS_LOOKUP_ID); 1060 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/data", 1061 CONTACTS_LOOKUP_ID_DATA); 1062 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/display_photo", 1063 CONTACTS_LOOKUP_DISPLAY_PHOTO); 1064 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/display_photo", 1065 CONTACTS_LOOKUP_ID_DISPLAY_PHOTO); 1066 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/entities", 1067 CONTACTS_LOOKUP_ENTITIES); 1068 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/entities", 1069 CONTACTS_LOOKUP_ID_ENTITIES); 1070 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/stream_items", 1071 CONTACTS_LOOKUP_STREAM_ITEMS); 1072 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/stream_items", 1073 CONTACTS_LOOKUP_ID_STREAM_ITEMS); 1074 matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_vcard/*", CONTACTS_AS_VCARD); 1075 matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_multi_vcard/*", 1076 CONTACTS_AS_MULTI_VCARD); 1077 matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/", CONTACTS_STREQUENT); 1078 matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/filter/*", 1079 CONTACTS_STREQUENT_FILTER); 1080 matcher.addURI(ContactsContract.AUTHORITY, "contacts/group/*", CONTACTS_GROUP); 1081 matcher.addURI(ContactsContract.AUTHORITY, "contacts/frequent", CONTACTS_FREQUENT); 1082 1083 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts", RAW_CONTACTS); 1084 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#", RAW_CONTACTS_ID); 1085 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/data", RAW_CONTACTS_DATA); 1086 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/display_photo", 1087 RAW_CONTACTS_ID_DISPLAY_PHOTO); 1088 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/entity", RAW_CONTACT_ENTITY_ID); 1089 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/stream_items", 1090 RAW_CONTACTS_ID_STREAM_ITEMS); 1091 1092 matcher.addURI(ContactsContract.AUTHORITY, "raw_contact_entities", RAW_CONTACT_ENTITIES); 1093 1094 matcher.addURI(ContactsContract.AUTHORITY, "data", DATA); 1095 matcher.addURI(ContactsContract.AUTHORITY, "data/#", DATA_ID); 1096 matcher.addURI(ContactsContract.AUTHORITY, "data/phones", PHONES); 1097 matcher.addURI(ContactsContract.AUTHORITY, "data/phones/#", PHONES_ID); 1098 matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter", PHONES_FILTER); 1099 matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER); 1100 matcher.addURI(ContactsContract.AUTHORITY, "data/emails", EMAILS); 1101 matcher.addURI(ContactsContract.AUTHORITY, "data/emails/#", EMAILS_ID); 1102 matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup", EMAILS_LOOKUP); 1103 matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup/*", EMAILS_LOOKUP); 1104 matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter", EMAILS_FILTER); 1105 matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter/*", EMAILS_FILTER); 1106 matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS); 1107 matcher.addURI(ContactsContract.AUTHORITY, "data/postals/#", POSTALS_ID); 1108 /** "*" is in CSV form with data ids ("123,456,789") */ 1109 matcher.addURI(ContactsContract.AUTHORITY, "data/usagefeedback/*", DATA_USAGE_FEEDBACK_ID); 1110 1111 matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS); 1112 matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID); 1113 matcher.addURI(ContactsContract.AUTHORITY, "groups_summary", GROUPS_SUMMARY); 1114 1115 matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH, SYNCSTATE); 1116 matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH + "/#", 1117 SYNCSTATE_ID); 1118 1119 matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", PHONE_LOOKUP); 1120 matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions", 1121 AGGREGATION_EXCEPTIONS); 1122 matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions/*", 1123 AGGREGATION_EXCEPTION_ID); 1124 1125 matcher.addURI(ContactsContract.AUTHORITY, "settings", SETTINGS); 1126 1127 matcher.addURI(ContactsContract.AUTHORITY, "status_updates", STATUS_UPDATES); 1128 matcher.addURI(ContactsContract.AUTHORITY, "status_updates/#", STATUS_UPDATES_ID); 1129 1130 matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, 1131 SEARCH_SUGGESTIONS); 1132 matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", 1133 SEARCH_SUGGESTIONS); 1134 matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*", 1135 SEARCH_SHORTCUT); 1136 1137 matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts", 1138 LIVE_FOLDERS_CONTACTS); 1139 matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts/*", 1140 LIVE_FOLDERS_CONTACTS_GROUP_NAME); 1141 matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts_with_phones", 1142 LIVE_FOLDERS_CONTACTS_WITH_PHONES); 1143 matcher.addURI(ContactsContract.AUTHORITY, "live_folders/favorites", 1144 LIVE_FOLDERS_CONTACTS_FAVORITES); 1145 1146 matcher.addURI(ContactsContract.AUTHORITY, "provider_status", PROVIDER_STATUS); 1147 1148 matcher.addURI(ContactsContract.AUTHORITY, "directories", DIRECTORIES); 1149 matcher.addURI(ContactsContract.AUTHORITY, "directories/#", DIRECTORIES_ID); 1150 1151 matcher.addURI(ContactsContract.AUTHORITY, "complete_name", COMPLETE_NAME); 1152 1153 matcher.addURI(ContactsContract.AUTHORITY, "profile", PROFILE); 1154 matcher.addURI(ContactsContract.AUTHORITY, "profile/entities", PROFILE_ENTITIES); 1155 matcher.addURI(ContactsContract.AUTHORITY, "profile/data", PROFILE_DATA); 1156 matcher.addURI(ContactsContract.AUTHORITY, "profile/data/#", PROFILE_DATA_ID); 1157 matcher.addURI(ContactsContract.AUTHORITY, "profile/as_vcard", PROFILE_AS_VCARD); 1158 matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contacts", PROFILE_RAW_CONTACTS); 1159 matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contacts/#", 1160 PROFILE_RAW_CONTACTS_ID); 1161 matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contacts/#/data", 1162 PROFILE_RAW_CONTACTS_ID_DATA); 1163 matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contacts/#/entity", 1164 PROFILE_RAW_CONTACTS_ID_ENTITIES); 1165 1166 matcher.addURI(ContactsContract.AUTHORITY, "stream_items", STREAM_ITEMS); 1167 matcher.addURI(ContactsContract.AUTHORITY, "stream_items/photo", STREAM_ITEMS_PHOTOS); 1168 matcher.addURI(ContactsContract.AUTHORITY, "stream_items/#", STREAM_ITEMS_ID); 1169 matcher.addURI(ContactsContract.AUTHORITY, "stream_items/#/photo", STREAM_ITEMS_ID_PHOTOS); 1170 matcher.addURI(ContactsContract.AUTHORITY, "stream_items/#/photo/#", 1171 STREAM_ITEMS_ID_PHOTOS_ID); 1172 matcher.addURI(ContactsContract.AUTHORITY, "stream_items_limit", STREAM_ITEMS_LIMIT); 1173 1174 matcher.addURI(ContactsContract.AUTHORITY, "display_photo/*", DISPLAY_PHOTO); 1175 matcher.addURI(ContactsContract.AUTHORITY, "photo_dimensions", PHOTO_DIMENSIONS); 1176 1177 HashMap<String, Integer> tmpTypeMap = new HashMap<String, Integer>(); 1178 tmpTypeMap.put(DataUsageFeedback.USAGE_TYPE_CALL, DataUsageStatColumns.USAGE_TYPE_INT_CALL); 1179 tmpTypeMap.put(DataUsageFeedback.USAGE_TYPE_LONG_TEXT, 1180 DataUsageStatColumns.USAGE_TYPE_INT_LONG_TEXT); 1181 tmpTypeMap.put(DataUsageFeedback.USAGE_TYPE_SHORT_TEXT, 1182 DataUsageStatColumns.USAGE_TYPE_INT_SHORT_TEXT); 1183 sDataUsageTypeMap = Collections.unmodifiableMap(tmpTypeMap); 1184 } 1185 1186 private static class DirectoryInfo { 1187 String authority; 1188 String accountName; 1189 String accountType; 1190 } 1191 1192 /** 1193 * Cached information about contact directories. 1194 */ 1195 private HashMap<String, DirectoryInfo> mDirectoryCache = new HashMap<String, DirectoryInfo>(); 1196 private boolean mDirectoryCacheValid = false; 1197 1198 /** 1199 * An entry in group id cache. It maps the combination of (account type, account name, data set, 1200 * and source id) to group row id. 1201 */ 1202 public static class GroupIdCacheEntry { 1203 String accountType; 1204 String accountName; 1205 String dataSet; 1206 String sourceId; 1207 long groupId; 1208 } 1209 1210 // We don't need a soft cache for groups - the assumption is that there will only 1211 // be a small number of contact groups. The cache is keyed off source id. The value 1212 // is a list of groups with this group id. 1213 private HashMap<String, ArrayList<GroupIdCacheEntry>> mGroupIdCache = Maps.newHashMap(); 1214 1215 /** 1216 * Cached information about the contact ID and raw contact IDs that make up the user's 1217 * profile entry. 1218 */ 1219 private static class ProfileIdCache { 1220 boolean inited; 1221 long profileContactId; 1222 Set<Long> profileRawContactIds = Sets.newHashSet(); 1223 Set<Long> profileDataIds = Sets.newHashSet(); 1224 1225 /** 1226 * Initializes the cache of profile contact and raw contact IDs. Does nothing if 1227 * the cache is already initialized (unless forceRefresh is set to true). 1228 * @param db The contacts database. 1229 * @param forceRefresh Whether to force re-initialization of the cache. 1230 */ 1231 private void init(SQLiteDatabase db, boolean forceRefresh) { 1232 if (!inited || forceRefresh) { 1233 profileContactId = 0; 1234 profileRawContactIds.clear(); 1235 profileDataIds.clear(); 1236 Cursor c = db.rawQuery("SELECT " + 1237 RawContactsColumns.CONCRETE_CONTACT_ID + "," + 1238 RawContactsColumns.CONCRETE_ID + "," + 1239 DataColumns.CONCRETE_ID + 1240 " FROM " + Tables.RAW_CONTACTS + " JOIN " + Tables.ACCOUNTS + " ON " + 1241 RawContactsColumns.CONCRETE_ID + "=" + 1242 AccountsColumns.PROFILE_RAW_CONTACT_ID + 1243 " JOIN " + Tables.DATA + " ON " + 1244 RawContactsColumns.CONCRETE_ID + "=" + DataColumns.CONCRETE_RAW_CONTACT_ID, 1245 null); 1246 try { 1247 while (c.moveToNext()) { 1248 if (profileContactId == 0) { 1249 profileContactId = c.getLong(0); 1250 } 1251 profileRawContactIds.add(c.getLong(1)); 1252 profileDataIds.add(c.getLong(2)); 1253 } 1254 } finally { 1255 c.close(); 1256 } 1257 } 1258 } 1259 } 1260 1261 private ProfileIdCache mProfileIdCache; 1262 1263 /** 1264 * Maximum dimension (height or width) of display photos. Larger images will be scaled 1265 * to fit. 1266 */ 1267 private int mMaxDisplayPhotoDim; 1268 1269 /** 1270 * Maximum dimension (height or width) of photo thumbnails. 1271 */ 1272 private int mMaxThumbnailPhotoDim; 1273 1274 private HashMap<String, DataRowHandler> mDataRowHandlers; 1275 private ContactsDatabaseHelper mDbHelper; 1276 1277 private PhotoStore mPhotoStore; 1278 1279 private NameSplitter mNameSplitter; 1280 private NameLookupBuilder mNameLookupBuilder; 1281 1282 private PostalSplitter mPostalSplitter; 1283 1284 private ContactDirectoryManager mContactDirectoryManager; 1285 private ContactAggregator mContactAggregator; 1286 private LegacyApiSupport mLegacyApiSupport; 1287 private GlobalSearchSupport mGlobalSearchSupport; 1288 private CommonNicknameCache mCommonNicknameCache; 1289 private SearchIndexManager mSearchIndexManager; 1290 1291 private ContentValues mValues = new ContentValues(); 1292 private HashMap<String, Boolean> mAccountWritability = Maps.newHashMap(); 1293 1294 private int mProviderStatus = ProviderStatus.STATUS_NORMAL; 1295 private boolean mProviderStatusUpdateNeeded; 1296 private long mEstimatedStorageRequirement = 0; 1297 private volatile CountDownLatch mReadAccessLatch; 1298 private volatile CountDownLatch mWriteAccessLatch; 1299 private boolean mAccountUpdateListenerRegistered; 1300 private boolean mOkToOpenAccess = true; 1301 1302 private TransactionContext mTransactionContext = new TransactionContext(); 1303 1304 private boolean mVisibleTouched = false; 1305 1306 private boolean mSyncToNetwork; 1307 1308 private Locale mCurrentLocale; 1309 private int mContactsAccountCount; 1310 1311 private HandlerThread mBackgroundThread; 1312 private Handler mBackgroundHandler; 1313 1314 private long mLastPhotoCleanup = 0; 1315 1316 @Override 1317 public boolean onCreate() { 1318 super.onCreate(); 1319 try { 1320 return initialize(); 1321 } catch (RuntimeException e) { 1322 Log.e(TAG, "Cannot start provider", e); 1323 return false; 1324 } 1325 } 1326 1327 private boolean initialize() { 1328 StrictMode.setThreadPolicy( 1329 new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build()); 1330 1331 Resources resources = getContext().getResources(); 1332 mMaxDisplayPhotoDim = resources.getInteger( 1333 R.integer.config_max_display_photo_dim); 1334 mMaxThumbnailPhotoDim = resources.getInteger( 1335 R.integer.config_max_thumbnail_photo_dim); 1336 1337 mProfileIdCache = new ProfileIdCache(); 1338 mDbHelper = (ContactsDatabaseHelper)getDatabaseHelper(); 1339 mContactDirectoryManager = new ContactDirectoryManager(this); 1340 mGlobalSearchSupport = new GlobalSearchSupport(this); 1341 1342 // The provider is closed for business until fully initialized 1343 mReadAccessLatch = new CountDownLatch(1); 1344 mWriteAccessLatch = new CountDownLatch(1); 1345 1346 mBackgroundThread = new HandlerThread("ContactsProviderWorker", 1347 Process.THREAD_PRIORITY_BACKGROUND); 1348 mBackgroundThread.start(); 1349 mBackgroundHandler = new Handler(mBackgroundThread.getLooper()) { 1350 @Override 1351 public void handleMessage(Message msg) { 1352 performBackgroundTask(msg.what, msg.obj); 1353 } 1354 }; 1355 1356 scheduleBackgroundTask(BACKGROUND_TASK_INITIALIZE); 1357 scheduleBackgroundTask(BACKGROUND_TASK_IMPORT_LEGACY_CONTACTS); 1358 scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_ACCOUNTS); 1359 scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_LOCALE); 1360 scheduleBackgroundTask(BACKGROUND_TASK_UPGRADE_AGGREGATION_ALGORITHM); 1361 scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_SEARCH_INDEX); 1362 scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_PROVIDER_STATUS); 1363 scheduleBackgroundTask(BACKGROUND_TASK_OPEN_WRITE_ACCESS); 1364 scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS); 1365 1366 return true; 1367 } 1368 1369 /** 1370 * (Re)allocates all locale-sensitive structures. 1371 */ 1372 private void initForDefaultLocale() { 1373 Context context = getContext(); 1374 mLegacyApiSupport = new LegacyApiSupport(context, mDbHelper, this, mGlobalSearchSupport); 1375 mCurrentLocale = getLocale(); 1376 mNameSplitter = mDbHelper.createNameSplitter(); 1377 mNameLookupBuilder = new StructuredNameLookupBuilder(mNameSplitter); 1378 mPostalSplitter = new PostalSplitter(mCurrentLocale); 1379 mCommonNicknameCache = new CommonNicknameCache(mDbHelper.getReadableDatabase()); 1380 ContactLocaleUtils.getIntance().setLocale(mCurrentLocale); 1381 mContactAggregator = new ContactAggregator(this, mDbHelper, 1382 createPhotoPriorityResolver(context), mNameSplitter, mCommonNicknameCache); 1383 mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true)); 1384 mSearchIndexManager = new SearchIndexManager(this); 1385 mPhotoStore = new PhotoStore(getContext().getFilesDir(), mDbHelper); 1386 1387 mDataRowHandlers = new HashMap<String, DataRowHandler>(); 1388 1389 mDataRowHandlers.put(Email.CONTENT_ITEM_TYPE, 1390 new DataRowHandlerForEmail(context, mDbHelper, mContactAggregator)); 1391 mDataRowHandlers.put(Im.CONTENT_ITEM_TYPE, 1392 new DataRowHandlerForIm(context, mDbHelper, mContactAggregator)); 1393 mDataRowHandlers.put(Organization.CONTENT_ITEM_TYPE, 1394 new DataRowHandlerForOrganization(context, mDbHelper, mContactAggregator)); 1395 mDataRowHandlers.put(Phone.CONTENT_ITEM_TYPE, 1396 new DataRowHandlerForPhoneNumber(context, mDbHelper, mContactAggregator)); 1397 mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, 1398 new DataRowHandlerForNickname(context, mDbHelper, mContactAggregator)); 1399 mDataRowHandlers.put(StructuredName.CONTENT_ITEM_TYPE, 1400 new DataRowHandlerForStructuredName(context, mDbHelper, mContactAggregator, 1401 mNameSplitter, mNameLookupBuilder)); 1402 mDataRowHandlers.put(StructuredPostal.CONTENT_ITEM_TYPE, 1403 new DataRowHandlerForStructuredPostal(context, mDbHelper, mContactAggregator, 1404 mPostalSplitter)); 1405 mDataRowHandlers.put(GroupMembership.CONTENT_ITEM_TYPE, 1406 new DataRowHandlerForGroupMembership(context, mDbHelper, mContactAggregator, 1407 mGroupIdCache)); 1408 mDataRowHandlers.put(Photo.CONTENT_ITEM_TYPE, 1409 new DataRowHandlerForPhoto(context, mDbHelper, mContactAggregator, mPhotoStore)); 1410 mDataRowHandlers.put(Note.CONTENT_ITEM_TYPE, 1411 new DataRowHandlerForNote(context, mDbHelper, mContactAggregator)); 1412 } 1413 1414 /** 1415 * Visible for testing. 1416 */ 1417 /* package */ PhotoPriorityResolver createPhotoPriorityResolver(Context context) { 1418 return new PhotoPriorityResolver(context); 1419 } 1420 1421 protected void scheduleBackgroundTask(int task) { 1422 mBackgroundHandler.sendEmptyMessage(task); 1423 } 1424 1425 protected void scheduleBackgroundTask(int task, Object arg) { 1426 mBackgroundHandler.sendMessage(mBackgroundHandler.obtainMessage(task, arg)); 1427 } 1428 1429 protected void performBackgroundTask(int task, Object arg) { 1430 switch (task) { 1431 case BACKGROUND_TASK_INITIALIZE: { 1432 initForDefaultLocale(); 1433 mReadAccessLatch.countDown(); 1434 mReadAccessLatch = null; 1435 break; 1436 } 1437 1438 case BACKGROUND_TASK_OPEN_WRITE_ACCESS: { 1439 if (mOkToOpenAccess) { 1440 mWriteAccessLatch.countDown(); 1441 mWriteAccessLatch = null; 1442 } 1443 break; 1444 } 1445 1446 case BACKGROUND_TASK_IMPORT_LEGACY_CONTACTS: { 1447 if (isLegacyContactImportNeeded()) { 1448 importLegacyContactsInBackground(); 1449 } 1450 break; 1451 } 1452 1453 case BACKGROUND_TASK_UPDATE_ACCOUNTS: { 1454 Context context = getContext(); 1455 if (!mAccountUpdateListenerRegistered) { 1456 AccountManager.get(context).addOnAccountsUpdatedListener(this, null, false); 1457 mAccountUpdateListenerRegistered = true; 1458 } 1459 1460 Account[] accounts = AccountManager.get(context).getAccounts(); 1461 boolean accountsChanged = updateAccountsInBackground(accounts); 1462 updateContactsAccountCount(accounts); 1463 updateDirectoriesInBackground(accountsChanged); 1464 break; 1465 } 1466 1467 case BACKGROUND_TASK_UPDATE_LOCALE: { 1468 updateLocaleInBackground(); 1469 break; 1470 } 1471 1472 case BACKGROUND_TASK_CHANGE_LOCALE: { 1473 changeLocaleInBackground(); 1474 break; 1475 } 1476 1477 case BACKGROUND_TASK_UPGRADE_AGGREGATION_ALGORITHM: { 1478 if (isAggregationUpgradeNeeded()) { 1479 upgradeAggregationAlgorithmInBackground(); 1480 } 1481 break; 1482 } 1483 1484 case BACKGROUND_TASK_UPDATE_SEARCH_INDEX: { 1485 updateSearchIndexInBackground(); 1486 break; 1487 } 1488 1489 case BACKGROUND_TASK_UPDATE_PROVIDER_STATUS: { 1490 updateProviderStatus(); 1491 break; 1492 } 1493 1494 case BACKGROUND_TASK_UPDATE_DIRECTORIES: { 1495 if (arg != null) { 1496 mContactDirectoryManager.onPackageChanged((String) arg); 1497 } 1498 break; 1499 } 1500 1501 case BACKGROUND_TASK_CLEANUP_PHOTOS: { 1502 // Check rate limit. 1503 long now = System.currentTimeMillis(); 1504 if (now - mLastPhotoCleanup > PHOTO_CLEANUP_RATE_LIMIT) { 1505 mLastPhotoCleanup = now; 1506 cleanupPhotoStore(); 1507 break; 1508 } 1509 } 1510 } 1511 } 1512 1513 public void onLocaleChanged() { 1514 if (mProviderStatus != ProviderStatus.STATUS_NORMAL 1515 && mProviderStatus != ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS) { 1516 return; 1517 } 1518 1519 scheduleBackgroundTask(BACKGROUND_TASK_CHANGE_LOCALE); 1520 } 1521 1522 /** 1523 * Verifies that the contacts database is properly configured for the current locale. 1524 * If not, changes the database locale to the current locale using an asynchronous task. 1525 * This needs to be done asynchronously because the process involves rebuilding 1526 * large data structures (name lookup, sort keys), which can take minutes on 1527 * a large set of contacts. 1528 */ 1529 protected void updateLocaleInBackground() { 1530 1531 // The process is already running - postpone the change 1532 if (mProviderStatus == ProviderStatus.STATUS_CHANGING_LOCALE) { 1533 return; 1534 } 1535 1536 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); 1537 final String providerLocale = prefs.getString(PREF_LOCALE, null); 1538 final Locale currentLocale = mCurrentLocale; 1539 if (currentLocale.toString().equals(providerLocale)) { 1540 return; 1541 } 1542 1543 int providerStatus = mProviderStatus; 1544 setProviderStatus(ProviderStatus.STATUS_CHANGING_LOCALE); 1545 mDbHelper.setLocale(this, currentLocale); 1546 prefs.edit().putString(PREF_LOCALE, currentLocale.toString()).apply(); 1547 setProviderStatus(providerStatus); 1548 } 1549 1550 /** 1551 * Reinitializes the provider for a new locale. 1552 */ 1553 private void changeLocaleInBackground() { 1554 // Re-initializing the provider without stopping it. 1555 // Locking the database will prevent inserts/updates/deletes from 1556 // running at the same time, but queries may still be running 1557 // on other threads. Those queries may return inconsistent results. 1558 SQLiteDatabase db = mDbHelper.getWritableDatabase(); 1559 db.beginTransaction(); 1560 try { 1561 initForDefaultLocale(); 1562 db.setTransactionSuccessful(); 1563 } finally { 1564 db.endTransaction(); 1565 } 1566 1567 updateLocaleInBackground(); 1568 } 1569 1570 protected void updateSearchIndexInBackground() { 1571 mSearchIndexManager.updateIndex(); 1572 } 1573 1574 protected void updateDirectoriesInBackground(boolean rescan) { 1575 mContactDirectoryManager.scanAllPackages(rescan); 1576 } 1577 1578 private void updateProviderStatus() { 1579 if (mProviderStatus != ProviderStatus.STATUS_NORMAL 1580 && mProviderStatus != ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS) { 1581 return; 1582 } 1583 1584 if (mContactsAccountCount == 0 1585 && DatabaseUtils.queryNumEntries(mDbHelper.getReadableDatabase(), 1586 Tables.CONTACTS, null) == 0) { 1587 setProviderStatus(ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS); 1588 } else { 1589 setProviderStatus(ProviderStatus.STATUS_NORMAL); 1590 } 1591 } 1592 1593 /* Visible for testing */ 1594 protected void cleanupPhotoStore() { 1595 SQLiteDatabase db = mDbHelper.getWritableDatabase(); 1596 1597 // Assemble the set of photo store file IDs that are in use, and send those to the photo 1598 // store. Any photos that aren't in that set will be deleted, and any photos that no 1599 // longer exist in the photo store will be returned for us to clear out in the DB. 1600 Cursor c = db.query(Views.DATA, new String[]{Data._ID, Photo.PHOTO_FILE_ID}, 1601 Data.MIMETYPE + "=" + Photo.MIMETYPE + " AND " 1602 + Photo.PHOTO_FILE_ID + " IS NOT NULL", null, null, null, null); 1603 Set<Long> usedPhotoFileIds = Sets.newHashSet(); 1604 Map<Long, Long> photoFileIdToDataId = Maps.newHashMap(); 1605 try { 1606 while (c.moveToNext()) { 1607 long dataId = c.getLong(0); 1608 long photoFileId = c.getLong(1); 1609 usedPhotoFileIds.add(photoFileId); 1610 photoFileIdToDataId.put(photoFileId, dataId); 1611 } 1612 } finally { 1613 c.close(); 1614 } 1615 1616 // Also query for all social stream item photos. 1617 c = db.query(Tables.STREAM_ITEM_PHOTOS, 1618 new String[]{ 1619 StreamItemPhotos._ID, 1620 StreamItemPhotos.STREAM_ITEM_ID, 1621 StreamItemPhotos.PHOTO_FILE_ID 1622 }, 1623 null, null, null, null, null); 1624 Map<Long, Long> photoFileIdToStreamItemPhotoId = Maps.newHashMap(); 1625 Map<Long, Long> streamItemPhotoIdToStreamItemId = Maps.newHashMap(); 1626 try { 1627 while (c.moveToNext()) { 1628 long streamItemPhotoId = c.getLong(0); 1629 long streamItemId = c.getLong(1); 1630 long photoFileId = c.getLong(2); 1631 usedPhotoFileIds.add(photoFileId); 1632 photoFileIdToStreamItemPhotoId.put(photoFileId, streamItemPhotoId); 1633 streamItemPhotoIdToStreamItemId.put(streamItemPhotoId, streamItemId); 1634 } 1635 } finally { 1636 c.close(); 1637 } 1638 1639 // Run the photo store cleanup. 1640 Set<Long> missingPhotoIds = mPhotoStore.cleanup(usedPhotoFileIds); 1641 1642 // If any of the keys we're using no longer exist, clean them up. 1643 if (!missingPhotoIds.isEmpty()) { 1644 ArrayList<ContentProviderOperation> ops = Lists.newArrayList(); 1645 for (long missingPhotoId : missingPhotoIds) { 1646 if (photoFileIdToDataId.containsKey(missingPhotoId)) { 1647 long dataId = photoFileIdToDataId.get(missingPhotoId); 1648 ContentValues updateValues = new ContentValues(); 1649 updateValues.putNull(Photo.PHOTO_FILE_ID); 1650 ops.add(ContentProviderOperation.newUpdate( 1651 ContentUris.withAppendedId(Data.CONTENT_URI, dataId)) 1652 .withValues(updateValues).build()); 1653 } 1654 if (photoFileIdToStreamItemPhotoId.containsKey(missingPhotoId)) { 1655 // For missing photos that were in stream item photos, just delete the stream 1656 // item photo. 1657 long streamItemPhotoId = photoFileIdToStreamItemPhotoId.get(missingPhotoId); 1658 long streamItemId = streamItemPhotoIdToStreamItemId.get(streamItemPhotoId); 1659 ops.add(ContentProviderOperation.newDelete( 1660 StreamItems.CONTENT_URI.buildUpon() 1661 .appendPath(String.valueOf(streamItemId)) 1662 .appendPath(StreamItems.StreamItemPhotos.CONTENT_DIRECTORY) 1663 .appendPath(String.valueOf(streamItemPhotoId)) 1664 .build()).build()); 1665 } 1666 } 1667 try { 1668 applyBatch(ops); 1669 } catch (OperationApplicationException oae) { 1670 // Not a fatal problem (and we'll try again on the next cleanup). 1671 Log.e(TAG, "Failed to clean up outdated photo references", oae); 1672 } 1673 } 1674 } 1675 1676 /* Visible for testing */ 1677 @Override 1678 protected ContactsDatabaseHelper getDatabaseHelper(final Context context) { 1679 return ContactsDatabaseHelper.getInstance(context); 1680 } 1681 1682 @VisibleForTesting 1683 /* package */ PhotoStore getPhotoStore() { 1684 return mPhotoStore; 1685 } 1686 1687 /* package */ int getMaxDisplayPhotoDim() { 1688 return mMaxDisplayPhotoDim; 1689 } 1690 1691 /* package */ int getMaxThumbnailPhotoDim() { 1692 return mMaxThumbnailPhotoDim; 1693 } 1694 1695 /* package */ NameSplitter getNameSplitter() { 1696 return mNameSplitter; 1697 } 1698 1699 /* package */ NameLookupBuilder getNameLookupBuilder() { 1700 return mNameLookupBuilder; 1701 } 1702 1703 /* Visible for testing */ 1704 public ContactDirectoryManager getContactDirectoryManagerForTest() { 1705 return mContactDirectoryManager; 1706 } 1707 1708 /* Visible for testing */ 1709 protected Locale getLocale() { 1710 return Locale.getDefault(); 1711 } 1712 1713 protected boolean isLegacyContactImportNeeded() { 1714 int version = Integer.parseInt(mDbHelper.getProperty(PROPERTY_CONTACTS_IMPORTED, "0")); 1715 return version < PROPERTY_CONTACTS_IMPORT_VERSION; 1716 } 1717 1718 protected LegacyContactImporter getLegacyContactImporter() { 1719 return new LegacyContactImporter(getContext(), this); 1720 } 1721 1722 /** 1723 * Imports legacy contacts as a background task. 1724 */ 1725 private void importLegacyContactsInBackground() { 1726 Log.v(TAG, "Importing legacy contacts"); 1727 setProviderStatus(ProviderStatus.STATUS_UPGRADING); 1728 1729 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); 1730 mDbHelper.setLocale(this, mCurrentLocale); 1731 prefs.edit().putString(PREF_LOCALE, mCurrentLocale.toString()).commit(); 1732 1733 LegacyContactImporter importer = getLegacyContactImporter(); 1734 if (importLegacyContacts(importer)) { 1735 onLegacyContactImportSuccess(); 1736 } else { 1737 onLegacyContactImportFailure(); 1738 } 1739 } 1740 1741 /** 1742 * Unlocks the provider and declares that the import process is complete. 1743 */ 1744 private void onLegacyContactImportSuccess() { 1745 NotificationManager nm = 1746 (NotificationManager)getContext().getSystemService(Context.NOTIFICATION_SERVICE); 1747 nm.cancel(LEGACY_IMPORT_FAILED_NOTIFICATION); 1748 1749 // Store a property in the database indicating that the conversion process succeeded 1750 mDbHelper.setProperty(PROPERTY_CONTACTS_IMPORTED, 1751 String.valueOf(PROPERTY_CONTACTS_IMPORT_VERSION)); 1752 setProviderStatus(ProviderStatus.STATUS_NORMAL); 1753 Log.v(TAG, "Completed import of legacy contacts"); 1754 } 1755 1756 /** 1757 * Announces the provider status and keeps the provider locked. 1758 */ 1759 private void onLegacyContactImportFailure() { 1760 Context context = getContext(); 1761 NotificationManager nm = 1762 (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); 1763 1764 // Show a notification 1765 Notification n = new Notification(android.R.drawable.stat_notify_error, 1766 context.getString(R.string.upgrade_out_of_memory_notification_ticker), 1767 System.currentTimeMillis()); 1768 n.setLatestEventInfo(context, 1769 context.getString(R.string.upgrade_out_of_memory_notification_title), 1770 context.getString(R.string.upgrade_out_of_memory_notification_text), 1771 PendingIntent.getActivity(context, 0, new Intent(Intents.UI.LIST_DEFAULT), 0)); 1772 n.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT; 1773 1774 nm.notify(LEGACY_IMPORT_FAILED_NOTIFICATION, n); 1775 1776 setProviderStatus(ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY); 1777 Log.v(TAG, "Failed to import legacy contacts"); 1778 1779 // Do not let any database changes until this issue is resolved. 1780 mOkToOpenAccess = false; 1781 } 1782 1783 /* Visible for testing */ 1784 /* package */ boolean importLegacyContacts(LegacyContactImporter importer) { 1785 boolean aggregatorEnabled = mContactAggregator.isEnabled(); 1786 mContactAggregator.setEnabled(false); 1787 try { 1788 if (importer.importContacts()) { 1789 1790 // TODO aggregate all newly added raw contacts 1791 mContactAggregator.setEnabled(aggregatorEnabled); 1792 return true; 1793 } 1794 } catch (Throwable e) { 1795 Log.e(TAG, "Legacy contact import failed", e); 1796 } 1797 mEstimatedStorageRequirement = importer.getEstimatedStorageRequirement(); 1798 return false; 1799 } 1800 1801 /** 1802 * Wipes all data from the contacts database. 1803 */ 1804 /* package */ void wipeData() { 1805 mDbHelper.wipeData(); 1806 mPhotoStore.clear(); 1807 mProviderStatus = ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS; 1808 } 1809 1810 /** 1811 * During intialization, this content provider will 1812 * block all attempts to change contacts data. In particular, it will hold 1813 * up all contact syncs. As soon as the import process is complete, all 1814 * processes waiting to write to the provider are unblocked and can proceed 1815 * to compete for the database transaction monitor. 1816 */ 1817 private void waitForAccess(CountDownLatch latch) { 1818 if (latch == null) { 1819 return; 1820 } 1821 1822 while (true) { 1823 try { 1824 latch.await(); 1825 return; 1826 } catch (InterruptedException e) { 1827 Thread.currentThread().interrupt(); 1828 } 1829 } 1830 } 1831 1832 @Override 1833 public Uri insert(Uri uri, ContentValues values) { 1834 waitForAccess(mWriteAccessLatch); 1835 return super.insert(uri, values); 1836 } 1837 1838 @Override 1839 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 1840 if (mWriteAccessLatch != null) { 1841 // We are stuck trying to upgrade contacts db. The only update request 1842 // allowed in this case is an update of provider status, which will trigger 1843 // an attempt to upgrade contacts again. 1844 int match = sUriMatcher.match(uri); 1845 if (match == PROVIDER_STATUS) { 1846 Integer newStatus = values.getAsInteger(ProviderStatus.STATUS); 1847 if (newStatus != null && newStatus == ProviderStatus.STATUS_UPGRADING) { 1848 scheduleBackgroundTask(BACKGROUND_TASK_IMPORT_LEGACY_CONTACTS); 1849 return 1; 1850 } else { 1851 return 0; 1852 } 1853 } 1854 } 1855 waitForAccess(mWriteAccessLatch); 1856 return super.update(uri, values, selection, selectionArgs); 1857 } 1858 1859 @Override 1860 public int delete(Uri uri, String selection, String[] selectionArgs) { 1861 waitForAccess(mWriteAccessLatch); 1862 return super.delete(uri, selection, selectionArgs); 1863 } 1864 1865 @Override 1866 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 1867 throws OperationApplicationException { 1868 waitForAccess(mWriteAccessLatch); 1869 return super.applyBatch(operations); 1870 } 1871 1872 @Override 1873 public int bulkInsert(Uri uri, ContentValues[] values) { 1874 waitForAccess(mWriteAccessLatch); 1875 return super.bulkInsert(uri, values); 1876 } 1877 1878 @Override 1879 protected void onBeginTransaction() { 1880 if (VERBOSE_LOGGING) { 1881 Log.v(TAG, "onBeginTransaction"); 1882 } 1883 super.onBeginTransaction(); 1884 mContactAggregator.clearPendingAggregations(); 1885 mTransactionContext.clear(); 1886 } 1887 1888 1889 @Override 1890 protected void beforeTransactionCommit() { 1891 1892 if (VERBOSE_LOGGING) { 1893 Log.v(TAG, "beforeTransactionCommit"); 1894 } 1895 super.beforeTransactionCommit(); 1896 flushTransactionalChanges(); 1897 mContactAggregator.aggregateInTransaction(mTransactionContext, mDb); 1898 if (mVisibleTouched) { 1899 mVisibleTouched = false; 1900 mDbHelper.updateAllVisible(); 1901 } 1902 1903 updateSearchIndexInTransaction(); 1904 1905 if (mProviderStatusUpdateNeeded) { 1906 updateProviderStatus(); 1907 mProviderStatusUpdateNeeded = false; 1908 } 1909 } 1910 1911 private void updateSearchIndexInTransaction() { 1912 Set<Long> staleContacts = mTransactionContext.getStaleSearchIndexContactIds(); 1913 Set<Long> staleRawContacts = mTransactionContext.getStaleSearchIndexRawContactIds(); 1914 if (!staleContacts.isEmpty() || !staleRawContacts.isEmpty()) { 1915 mSearchIndexManager.updateIndexForRawContacts(staleContacts, staleRawContacts); 1916 mTransactionContext.clearSearchIndexUpdates(); 1917 } 1918 } 1919 1920 private void flushTransactionalChanges() { 1921 if (VERBOSE_LOGGING) { 1922 Log.v(TAG, "flushTransactionChanges"); 1923 } 1924 1925 // Determine whether we need to refresh the profile ID cache. 1926 boolean profileCacheRefreshNeeded = false; 1927 1928 for (long rawContactId : mTransactionContext.getInsertedRawContactIds()) { 1929 mDbHelper.updateRawContactDisplayName(mDb, rawContactId); 1930 mContactAggregator.onRawContactInsert(mTransactionContext, mDb, rawContactId); 1931 } 1932 1933 Map<Long, AccountWithDataSet> insertedProfileRawContactAccountMap = 1934 mTransactionContext.getInsertedProfileRawContactIds(); 1935 if (!insertedProfileRawContactAccountMap.isEmpty()) { 1936 for (long profileRawContactId : insertedProfileRawContactAccountMap.keySet()) { 1937 mDbHelper.updateRawContactDisplayName(mDb, profileRawContactId); 1938 mContactAggregator.onProfileRawContactInsert(mTransactionContext, mDb, 1939 profileRawContactId, 1940 insertedProfileRawContactAccountMap.get(profileRawContactId)); 1941 } 1942 profileCacheRefreshNeeded = true; 1943 } 1944 1945 Set<Long> dirtyRawContacts = mTransactionContext.getDirtyRawContactIds(); 1946 if (!dirtyRawContacts.isEmpty()) { 1947 mSb.setLength(0); 1948 mSb.append(UPDATE_RAW_CONTACT_SET_DIRTY_SQL); 1949 appendIds(mSb, dirtyRawContacts); 1950 mSb.append(")"); 1951 mDb.execSQL(mSb.toString()); 1952 1953 profileCacheRefreshNeeded = profileCacheRefreshNeeded || 1954 !Collections.disjoint(mProfileIdCache.profileRawContactIds, dirtyRawContacts); 1955 } 1956 1957 Set<Long> updatedRawContacts = mTransactionContext.getUpdatedRawContactIds(); 1958 if (!updatedRawContacts.isEmpty()) { 1959 mSb.setLength(0); 1960 mSb.append(UPDATE_RAW_CONTACT_SET_VERSION_SQL); 1961 appendIds(mSb, updatedRawContacts); 1962 mSb.append(")"); 1963 mDb.execSQL(mSb.toString()); 1964 1965 profileCacheRefreshNeeded = profileCacheRefreshNeeded || 1966 !Collections.disjoint(mProfileIdCache.profileRawContactIds, updatedRawContacts); 1967 } 1968 1969 for (Map.Entry<Long, Object> entry : mTransactionContext.getUpdatedSyncStates()) { 1970 long id = entry.getKey(); 1971 if (mDbHelper.getSyncState().update(mDb, id, entry.getValue()) <= 0) { 1972 throw new IllegalStateException( 1973 "unable to update sync state, does it still exist?"); 1974 } 1975 } 1976 1977 if (profileCacheRefreshNeeded) { 1978 // Force the profile ID cache to refresh. 1979 mProfileIdCache.init(mDb, true); 1980 } 1981 1982 mTransactionContext.clear(); 1983 } 1984 1985 /** 1986 * Appends comma separated ids. 1987 * @param ids Should not be empty 1988 */ 1989 private void appendIds(StringBuilder sb, Set<Long> ids) { 1990 for (long id : ids) { 1991 sb.append(id).append(','); 1992 } 1993 1994 sb.setLength(sb.length() - 1); // Yank the last comma 1995 } 1996 1997 /** 1998 * Checks whether the given contact ID represents the user's personal profile - if it is, calls 1999 * a permission check (for writing the profile if forWrite is true, for reading the profile 2000 * otherwise). If the contact ID is not the user's profile, no check is executed. 2001 * @param db The database. 2002 * @param contactId The contact ID to be checked. 2003 * @param forWrite Whether the caller is attempting to do a write (vs. read) operation. 2004 */ 2005 private void enforceProfilePermissionForContact(SQLiteDatabase db, long contactId, 2006 boolean forWrite) { 2007 mProfileIdCache.init(db, false); 2008 if (mProfileIdCache.profileContactId == contactId) { 2009 enforceProfilePermission(forWrite); 2010 } 2011 } 2012 2013 /** 2014 * Checks whether the given raw contact ID is a member of the user's personal profile - if it 2015 * is, calls a permission check (for writing the profile if forWrite is true, for reading the 2016 * profile otherwise). If the raw contact ID is not in the user's profile, no check is 2017 * executed. 2018 * @param db The database. 2019 * @param rawContactId The raw contact ID to be checked. 2020 * @param forWrite Whether the caller is attempting to do a write (vs. read) operation. 2021 */ 2022 private void enforceProfilePermissionForRawContact(SQLiteDatabase db, long rawContactId, 2023 boolean forWrite) { 2024 mProfileIdCache.init(db, false); 2025 if (mProfileIdCache.profileRawContactIds.contains(rawContactId)) { 2026 enforceProfilePermission(forWrite); 2027 } 2028 } 2029 2030 /** 2031 * Checks whether the given data ID is a member of the user's personal profile - if it is, 2032 * calls a permission check (for writing the profile if forWrite is true, for reading the 2033 * profile otherwise). If the data ID is not in the user's profile, no check is executed. 2034 * @param db The database. 2035 * @param dataId The data ID to be checked. 2036 * @param forWrite Whether the caller is attempting to do a write (vs. read) operation. 2037 */ 2038 private void enforceProfilePermissionForData(SQLiteDatabase db, long dataId, boolean forWrite) { 2039 mProfileIdCache.init(db, false); 2040 if (mProfileIdCache.profileDataIds.contains(dataId)) { 2041 enforceProfilePermission(forWrite); 2042 } 2043 } 2044 2045 /** 2046 * Performs a permission check for WRITE_PROFILE or READ_PROFILE (depending on the parameter). 2047 * If the permission check fails, this will throw a SecurityException. 2048 * @param forWrite Whether the caller is attempting to do a write (vs. read) operation. 2049 */ 2050 private void enforceProfilePermission(boolean forWrite) { 2051 String profilePermission = forWrite 2052 ? "android.permission.WRITE_PROFILE" 2053 : "android.permission.READ_PROFILE"; 2054 getContext().enforceCallingOrSelfPermission(profilePermission, null); 2055 } 2056 2057 @Override 2058 protected void notifyChange() { 2059 notifyChange(mSyncToNetwork); 2060 mSyncToNetwork = false; 2061 } 2062 2063 protected void notifyChange(boolean syncToNetwork) { 2064 getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null, 2065 syncToNetwork); 2066 } 2067 2068 protected void setProviderStatus(int status) { 2069 if (mProviderStatus != status) { 2070 mProviderStatus = status; 2071 getContext().getContentResolver().notifyChange(ProviderStatus.CONTENT_URI, null, false); 2072 } 2073 } 2074 2075 public DataRowHandler getDataRowHandler(final String mimeType) { 2076 DataRowHandler handler = mDataRowHandlers.get(mimeType); 2077 if (handler == null) { 2078 handler = new DataRowHandlerForCustomMimetype( 2079 getContext(), mDbHelper, mContactAggregator, mimeType); 2080 mDataRowHandlers.put(mimeType, handler); 2081 } 2082 return handler; 2083 } 2084 2085 @Override 2086 protected Uri insertInTransaction(Uri uri, ContentValues values) { 2087 if (VERBOSE_LOGGING) { 2088 Log.v(TAG, "insertInTransaction: " + uri + " " + values); 2089 } 2090 2091 final boolean callerIsSyncAdapter = 2092 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); 2093 2094 final int match = sUriMatcher.match(uri); 2095 long id = 0; 2096 2097 switch (match) { 2098 case SYNCSTATE: 2099 id = mDbHelper.getSyncState().insert(mDb, values); 2100 break; 2101 2102 case CONTACTS: { 2103 insertContact(values); 2104 break; 2105 } 2106 2107 case PROFILE: { 2108 throw new UnsupportedOperationException( 2109 "The profile contact is created automatically"); 2110 } 2111 2112 case RAW_CONTACTS: { 2113 id = insertRawContact(uri, values, callerIsSyncAdapter, false); 2114 mSyncToNetwork |= !callerIsSyncAdapter; 2115 break; 2116 } 2117 2118 case RAW_CONTACTS_DATA: { 2119 values.put(Data.RAW_CONTACT_ID, uri.getPathSegments().get(1)); 2120 id = insertData(values, callerIsSyncAdapter); 2121 mSyncToNetwork |= !callerIsSyncAdapter; 2122 break; 2123 } 2124 2125 case RAW_CONTACTS_ID_STREAM_ITEMS: { 2126 values.put(StreamItems.RAW_CONTACT_ID, uri.getPathSegments().get(1)); 2127 id = insertStreamItem(uri, values); 2128 mSyncToNetwork |= !callerIsSyncAdapter; 2129 break; 2130 } 2131 2132 case PROFILE_RAW_CONTACTS: { 2133 enforceProfilePermission(true); 2134 id = insertRawContact(uri, values, callerIsSyncAdapter, true); 2135 mSyncToNetwork |= !callerIsSyncAdapter; 2136 break; 2137 } 2138 2139 case DATA: { 2140 id = insertData(values, callerIsSyncAdapter); 2141 mSyncToNetwork |= !callerIsSyncAdapter; 2142 break; 2143 } 2144 2145 case GROUPS: { 2146 id = insertGroup(uri, values, callerIsSyncAdapter); 2147 mSyncToNetwork |= !callerIsSyncAdapter; 2148 break; 2149 } 2150 2151 case SETTINGS: { 2152 id = insertSettings(uri, values); 2153 mSyncToNetwork |= !callerIsSyncAdapter; 2154 break; 2155 } 2156 2157 case STATUS_UPDATES: { 2158 id = insertStatusUpdate(values); 2159 break; 2160 } 2161 2162 case STREAM_ITEMS: { 2163 id = insertStreamItem(uri, values); 2164 mSyncToNetwork |= !callerIsSyncAdapter; 2165 break; 2166 } 2167 2168 case STREAM_ITEMS_PHOTOS: { 2169 id = insertStreamItemPhoto(uri, values); 2170 mSyncToNetwork |= !callerIsSyncAdapter; 2171 break; 2172 } 2173 2174 case STREAM_ITEMS_ID_PHOTOS: { 2175 values.put(StreamItemPhotos.STREAM_ITEM_ID, uri.getPathSegments().get(1)); 2176 id = insertStreamItemPhoto(uri, values); 2177 mSyncToNetwork |= !callerIsSyncAdapter; 2178 break; 2179 } 2180 2181 default: 2182 mSyncToNetwork = true; 2183 return mLegacyApiSupport.insert(uri, values); 2184 } 2185 2186 if (id < 0) { 2187 return null; 2188 } 2189 2190 return ContentUris.withAppendedId(uri, id); 2191 } 2192 2193 /** 2194 * If account is non-null then store it in the values. If the account is 2195 * already specified in the values then it must be consistent with the 2196 * account, if it is non-null. 2197 * 2198 * @param uri Current {@link Uri} being operated on. 2199 * @param values {@link ContentValues} to read and possibly update. 2200 * @throws IllegalArgumentException when only one of 2201 * {@link RawContacts#ACCOUNT_NAME} or 2202 * {@link RawContacts#ACCOUNT_TYPE} is specified, leaving the 2203 * other undefined. 2204 * @throws IllegalArgumentException when {@link RawContacts#ACCOUNT_NAME} 2205 * and {@link RawContacts#ACCOUNT_TYPE} are inconsistent between 2206 * the given {@link Uri} and {@link ContentValues}. 2207 */ 2208 private Account resolveAccount(Uri uri, ContentValues values) throws IllegalArgumentException { 2209 String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME); 2210 String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE); 2211 final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType); 2212 2213 String valueAccountName = values.getAsString(RawContacts.ACCOUNT_NAME); 2214 String valueAccountType = values.getAsString(RawContacts.ACCOUNT_TYPE); 2215 final boolean partialValues = TextUtils.isEmpty(valueAccountName) 2216 ^ TextUtils.isEmpty(valueAccountType); 2217 2218 if (partialUri || partialValues) { 2219 // Throw when either account is incomplete 2220 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 2221 "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri)); 2222 } 2223 2224 // Accounts are valid by only checking one parameter, since we've 2225 // already ruled out partial accounts. 2226 final boolean validUri = !TextUtils.isEmpty(accountName); 2227 final boolean validValues = !TextUtils.isEmpty(valueAccountName); 2228 2229 if (validValues && validUri) { 2230 // Check that accounts match when both present 2231 final boolean accountMatch = TextUtils.equals(accountName, valueAccountName) 2232 && TextUtils.equals(accountType, valueAccountType); 2233 if (!accountMatch) { 2234 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 2235 "When both specified, ACCOUNT_NAME and ACCOUNT_TYPE must match", uri)); 2236 } 2237 } else if (validUri) { 2238 // Fill values from Uri when not present 2239 values.put(RawContacts.ACCOUNT_NAME, accountName); 2240 values.put(RawContacts.ACCOUNT_TYPE, accountType); 2241 } else if (validValues) { 2242 accountName = valueAccountName; 2243 accountType = valueAccountType; 2244 } else { 2245 return null; 2246 } 2247 2248 // Use cached Account object when matches, otherwise create 2249 if (mAccount == null 2250 || !mAccount.name.equals(accountName) 2251 || !mAccount.type.equals(accountType)) { 2252 mAccount = new Account(accountName, accountType); 2253 } 2254 2255 return mAccount; 2256 } 2257 2258 /** 2259 * Resolves the account and builds an {@link AccountWithDataSet} based on the data set specified 2260 * in the URI or values (if any). 2261 * @param uri Current {@link Uri} being operated on. 2262 * @param values {@link ContentValues} to read and possibly update. 2263 */ 2264 private AccountWithDataSet resolveAccountWithDataSet(Uri uri, ContentValues values) { 2265 final Account account = resolveAccount(uri, mValues); 2266 AccountWithDataSet accountWithDataSet = null; 2267 if (account != null) { 2268 String dataSet = getQueryParameter(uri, RawContacts.DATA_SET); 2269 if (dataSet == null) { 2270 dataSet = mValues.getAsString(RawContacts.DATA_SET); 2271 } else { 2272 mValues.put(RawContacts.DATA_SET, dataSet); 2273 } 2274 accountWithDataSet = new AccountWithDataSet(account.name, account.type, dataSet); 2275 } 2276 return accountWithDataSet; 2277 } 2278 2279 /** 2280 * Inserts an item in the contacts table 2281 * 2282 * @param values the values for the new row 2283 * @return the row ID of the newly created row 2284 */ 2285 private long insertContact(ContentValues values) { 2286 throw new UnsupportedOperationException("Aggregate contacts are created automatically"); 2287 } 2288 2289 /** 2290 * Inserts an item in the raw contacts table 2291 * 2292 * @param uri the values for the new row 2293 * @param values the account this contact should be associated with. may be null. 2294 * @param callerIsSyncAdapter 2295 * @param forProfile Whether this raw contact is being inserted into the user's profile. 2296 * @return the row ID of the newly created row 2297 */ 2298 private long insertRawContact(Uri uri, ContentValues values, boolean callerIsSyncAdapter, 2299 boolean forProfile) { 2300 mValues.clear(); 2301 mValues.putAll(values); 2302 mValues.putNull(RawContacts.CONTACT_ID); 2303 2304 AccountWithDataSet accountWithDataSet = resolveAccountWithDataSet(uri, mValues); 2305 2306 if (values.containsKey(RawContacts.DELETED) 2307 && values.getAsInteger(RawContacts.DELETED) != 0) { 2308 mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED); 2309 } 2310 2311 long rawContactId = mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, mValues); 2312 int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT; 2313 if (forProfile) { 2314 // Profile raw contacts should never be aggregated by the aggregator; they are always 2315 // aggregated under a single profile contact. 2316 aggregationMode = RawContacts.AGGREGATION_MODE_DISABLED; 2317 } else if (mValues.containsKey(RawContacts.AGGREGATION_MODE)) { 2318 aggregationMode = mValues.getAsInteger(RawContacts.AGGREGATION_MODE); 2319 } 2320 mContactAggregator.markNewForAggregation(rawContactId, aggregationMode); 2321 2322 if (forProfile) { 2323 // Trigger creation of the user profile Contact (or association with the existing one) 2324 // at the end of the transaction. 2325 mTransactionContext.profileRawContactInserted(rawContactId, accountWithDataSet); 2326 } else { 2327 // Trigger creation of a Contact based on this RawContact at the end of transaction 2328 mTransactionContext.rawContactInserted(rawContactId, accountWithDataSet); 2329 } 2330 2331 if (!callerIsSyncAdapter) { 2332 addAutoAddMembership(rawContactId); 2333 final Long starred = values.getAsLong(RawContacts.STARRED); 2334 if (starred != null && starred != 0) { 2335 updateFavoritesMembership(rawContactId, starred != 0); 2336 } 2337 } 2338 2339 mProviderStatusUpdateNeeded = true; 2340 return rawContactId; 2341 } 2342 2343 private void addAutoAddMembership(long rawContactId) { 2344 final Long groupId = findGroupByRawContactId(SELECTION_AUTO_ADD_GROUPS_BY_RAW_CONTACT_ID, 2345 rawContactId); 2346 if (groupId != null) { 2347 insertDataGroupMembership(rawContactId, groupId); 2348 } 2349 } 2350 2351 private Long findGroupByRawContactId(String selection, long rawContactId) { 2352 Cursor c = mDb.query(Tables.GROUPS + "," + Tables.RAW_CONTACTS, PROJECTION_GROUP_ID, 2353 selection, 2354 new String[]{Long.toString(rawContactId)}, 2355 null /* groupBy */, null /* having */, null /* orderBy */); 2356 try { 2357 while (c.moveToNext()) { 2358 return c.getLong(0); 2359 } 2360 return null; 2361 } finally { 2362 c.close(); 2363 } 2364 } 2365 2366 private void updateFavoritesMembership(long rawContactId, boolean isStarred) { 2367 final Long groupId = findGroupByRawContactId(SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID, 2368 rawContactId); 2369 if (groupId != null) { 2370 if (isStarred) { 2371 insertDataGroupMembership(rawContactId, groupId); 2372 } else { 2373 deleteDataGroupMembership(rawContactId, groupId); 2374 } 2375 } 2376 } 2377 2378 private void insertDataGroupMembership(long rawContactId, long groupId) { 2379 ContentValues groupMembershipValues = new ContentValues(); 2380 groupMembershipValues.put(GroupMembership.GROUP_ROW_ID, groupId); 2381 groupMembershipValues.put(GroupMembership.RAW_CONTACT_ID, rawContactId); 2382 groupMembershipValues.put(DataColumns.MIMETYPE_ID, 2383 mDbHelper.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)); 2384 mDb.insert(Tables.DATA, null, groupMembershipValues); 2385 } 2386 2387 private void deleteDataGroupMembership(long rawContactId, long groupId) { 2388 final String[] selectionArgs = { 2389 Long.toString(mDbHelper.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)), 2390 Long.toString(groupId), 2391 Long.toString(rawContactId)}; 2392 mDb.delete(Tables.DATA, SELECTION_GROUPMEMBERSHIP_DATA, selectionArgs); 2393 } 2394 2395 /** 2396 * Inserts an item in the data table 2397 * 2398 * @param values the values for the new row 2399 * @return the row ID of the newly created row 2400 */ 2401 private long insertData(ContentValues values, boolean callerIsSyncAdapter) { 2402 long id = 0; 2403 mValues.clear(); 2404 mValues.putAll(values); 2405 2406 long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID); 2407 2408 // If the data being inserted belongs to the user's profile entry, check for the 2409 // WRITE_PROFILE permission before proceeding. 2410 enforceProfilePermissionForRawContact(mDb, rawContactId, true); 2411 2412 // Replace package with internal mapping 2413 final String packageName = mValues.getAsString(Data.RES_PACKAGE); 2414 if (packageName != null) { 2415 mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName)); 2416 } 2417 mValues.remove(Data.RES_PACKAGE); 2418 2419 // Replace mimetype with internal mapping 2420 final String mimeType = mValues.getAsString(Data.MIMETYPE); 2421 if (TextUtils.isEmpty(mimeType)) { 2422 throw new IllegalArgumentException(Data.MIMETYPE + " is required"); 2423 } 2424 2425 mValues.put(DataColumns.MIMETYPE_ID, mDbHelper.getMimeTypeId(mimeType)); 2426 mValues.remove(Data.MIMETYPE); 2427 2428 DataRowHandler rowHandler = getDataRowHandler(mimeType); 2429 id = rowHandler.insert(mDb, mTransactionContext, rawContactId, mValues); 2430 if (!callerIsSyncAdapter) { 2431 mTransactionContext.markRawContactDirty(rawContactId); 2432 } 2433 mTransactionContext.rawContactUpdated(rawContactId); 2434 return id; 2435 } 2436 2437 /** 2438 * Inserts an item in the stream_items table. The account is checked against the 2439 * account in the raw contact for which the stream item is being inserted. If the 2440 * new stream item results in more stream items under this raw contact than the limit, 2441 * the oldest one will be deleted (note that if the stream item inserted was the 2442 * oldest, it will be immediately deleted, and this will return 0). 2443 * 2444 * @param uri the insertion URI 2445 * @param values the values for the new row 2446 * @return the stream item _ID of the newly created row, or 0 if it was not created 2447 */ 2448 private long insertStreamItem(Uri uri, ContentValues values) { 2449 long id = 0; 2450 mValues.clear(); 2451 mValues.putAll(values); 2452 2453 long rawContactId = mValues.getAsLong(StreamItems.RAW_CONTACT_ID); 2454 2455 // If the data being inserted belongs to the user's profile entry, check for the 2456 // WRITE_PROFILE permission before proceeding. 2457 enforceProfilePermissionForRawContact(mDb, rawContactId, true); 2458 2459 // Ensure that the raw contact exists and belongs to the caller's account. 2460 Account account = resolveAccount(uri, mValues); 2461 enforceModifyingAccount(account, rawContactId); 2462 2463 // Don't attempt to insert accounts params - they don't exist in the stream items table. 2464 mValues.remove(RawContacts.ACCOUNT_NAME); 2465 mValues.remove(RawContacts.ACCOUNT_TYPE); 2466 2467 // Insert the new stream item. 2468 id = mDb.insert(Tables.STREAM_ITEMS, null, mValues); 2469 if (id == -1) { 2470 // Insertion failed. 2471 return 0; 2472 } 2473 2474 // Check to see if we're over the limit for stream items under this raw contact. 2475 // It's possible that the inserted stream item is older than the the existing 2476 // ones, in which case it may be deleted immediately (resetting the ID to 0). 2477 id = cleanUpOldStreamItems(rawContactId, id); 2478 2479 return id; 2480 } 2481 2482 /** 2483 * Inserts an item in the stream_item_photos table. The account is checked against 2484 * the account in the raw contact that owns the stream item being modified. 2485 * 2486 * @param uri the insertion URI 2487 * @param values the values for the new row 2488 * @return the stream item photo _ID of the newly created row, or 0 if there was an issue 2489 * with processing the photo or creating the row 2490 */ 2491 private long insertStreamItemPhoto(Uri uri, ContentValues values) { 2492 long id = 0; 2493 mValues.clear(); 2494 mValues.putAll(values); 2495 2496 long streamItemId = mValues.getAsLong(StreamItemPhotos.STREAM_ITEM_ID); 2497 if (streamItemId != 0) { 2498 long rawContactId = lookupRawContactIdForStreamId(streamItemId); 2499 2500 // If the data being inserted belongs to the user's profile entry, check for the 2501 // WRITE_PROFILE permission before proceeding. 2502 enforceProfilePermissionForRawContact(mDb, rawContactId, true); 2503 2504 // Ensure that the raw contact exists and belongs to the caller's account. 2505 Account account = resolveAccount(uri, mValues); 2506 enforceModifyingAccount(account, rawContactId); 2507 2508 // Don't attempt to insert accounts params - they don't exist in the stream item 2509 // photos table. 2510 mValues.remove(RawContacts.ACCOUNT_NAME); 2511 mValues.remove(RawContacts.ACCOUNT_TYPE); 2512 2513 // Process the photo and store it. 2514 if (processStreamItemPhoto(mValues, false)) { 2515 // Insert the stream item photo. 2516 id = mDb.insert(Tables.STREAM_ITEM_PHOTOS, null, mValues); 2517 } 2518 } 2519 return id; 2520 } 2521 2522 /** 2523 * Processes the photo contained in the {@link ContactsContract.StreamItemPhotos#PHOTO} 2524 * field of the given values, attempting to store it in the photo store. If successful, 2525 * the resulting photo file ID will be added to the values for insert/update in the table. 2526 * <p> 2527 * If updating, it is valid for the picture to be empty or unspecified (the function will 2528 * still return true). If inserting, a valid picture must be specified. 2529 * @param values The content values provided by the caller. 2530 * @param forUpdate Whether this photo is being processed for update (vs. insert). 2531 * @return Whether the insert or update should proceed. 2532 */ 2533 private boolean processStreamItemPhoto(ContentValues values, boolean forUpdate) { 2534 if (!values.containsKey(StreamItemPhotos.PHOTO)) { 2535 return forUpdate; 2536 } 2537 byte[] photoBytes = values.getAsByteArray(StreamItemPhotos.PHOTO); 2538 if (photoBytes == null) { 2539 return forUpdate; 2540 } 2541 2542 // Process the photo and store it. 2543 try { 2544 long photoFileId = mPhotoStore.insert(new PhotoProcessor(photoBytes, 2545 mMaxDisplayPhotoDim, mMaxThumbnailPhotoDim, true), true); 2546 if (photoFileId != 0) { 2547 values.put(StreamItemPhotos.PHOTO_FILE_ID, photoFileId); 2548 values.remove(StreamItemPhotos.PHOTO); 2549 return true; 2550 } else { 2551 // Couldn't store the photo, return 0. 2552 Log.e(TAG, "Could not process stream item photo for insert"); 2553 return false; 2554 } 2555 } catch (IOException ioe) { 2556 Log.e(TAG, "Could not process stream item photo for insert", ioe); 2557 return false; 2558 } 2559 } 2560 2561 /** 2562 * Looks up the raw contact ID that owns the specified stream item. 2563 * @param streamItemId The ID of the stream item. 2564 * @return The associated raw contact ID, or -1 if no such stream item exists. 2565 */ 2566 private long lookupRawContactIdForStreamId(long streamItemId) { 2567 long rawContactId = -1; 2568 Cursor c = mDb.query(Tables.STREAM_ITEMS, new String[]{StreamItems.RAW_CONTACT_ID}, 2569 StreamItems._ID + "=?", new String[]{String.valueOf(streamItemId)}, 2570 null, null, null); 2571 try { 2572 if (c.moveToFirst()) { 2573 rawContactId = c.getLong(0); 2574 } 2575 } finally { 2576 c.close(); 2577 } 2578 return rawContactId; 2579 } 2580 2581 /** 2582 * Checks whether the given raw contact ID is owned by the given account. 2583 * If the resolved account is null, this will return true iff the raw contact 2584 * is also associated with the "null" account. 2585 * 2586 * If the resolved account does not match, this will throw a security exception. 2587 * @param account The resolved account (may be null). 2588 * @param rawContactId The raw contact ID to check for. 2589 */ 2590 private void enforceModifyingAccount(Account account, long rawContactId) { 2591 String accountSelection = RawContactsColumns.CONCRETE_ID + "=? AND " 2592 + RawContactsColumns.CONCRETE_ACCOUNT_NAME + "=? AND " 2593 + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + "=?"; 2594 String noAccountSelection = RawContactsColumns.CONCRETE_ID + "=? AND " 2595 + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " IS NULL AND " 2596 + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " IS NULL"; 2597 Cursor c; 2598 if (account != null) { 2599 c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContactsColumns.CONCRETE_ID}, 2600 accountSelection, 2601 new String[]{String.valueOf(rawContactId), mAccount.name, mAccount.type}, 2602 null, null, null); 2603 } else { 2604 c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContactsColumns.CONCRETE_ID}, 2605 noAccountSelection, new String[]{String.valueOf(rawContactId)}, 2606 null, null, null); 2607 } 2608 try { 2609 if(c.getCount() == 0) { 2610 throw new SecurityException("Caller account does not match raw contact ID " 2611 + rawContactId); 2612 } 2613 } finally { 2614 c.close(); 2615 } 2616 } 2617 2618 /** 2619 * Checks whether the given selection of stream items matches up with the given 2620 * account. If any of the raw contacts fail the account check, this will throw a 2621 * security exception. 2622 * @param account The resolved account (may be null). 2623 * @param selection The selection. 2624 * @param selectionArgs The selection arguments. 2625 * @return The list of stream item IDs that would be included in this selection. 2626 */ 2627 private List<Long> enforceModifyingAccountForStreamItems(Account account, String selection, 2628 String[] selectionArgs) { 2629 List<Long> streamItemIds = Lists.newArrayList(); 2630 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 2631 setTablesAndProjectionMapForStreamItems(qb); 2632 Cursor c = qb.query(mDb, 2633 new String[]{StreamItems._ID, StreamItems.RAW_CONTACT_ID}, 2634 selection, selectionArgs, null, null, null); 2635 try { 2636 while (c.moveToNext()) { 2637 streamItemIds.add(c.getLong(0)); 2638 2639 // Throw a security exception if the account doesn't match the raw contact's. 2640 enforceModifyingAccount(account, c.getLong(1)); 2641 } 2642 } finally { 2643 c.close(); 2644 } 2645 return streamItemIds; 2646 } 2647 2648 /** 2649 * Checks whether the given selection of stream item photos matches up with the given 2650 * account. If any of the raw contacts fail the account check, this will throw a 2651 * security exception. 2652 * @param account The resolved account (may be null). 2653 * @param selection The selection. 2654 * @param selectionArgs The selection arguments. 2655 * @return The list of stream item photo IDs that would be included in this selection. 2656 */ 2657 private List<Long> enforceModifyingAccountForStreamItemPhotos(Account account, String selection, 2658 String[] selectionArgs) { 2659 List<Long> streamItemPhotoIds = Lists.newArrayList(); 2660 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 2661 setTablesAndProjectionMapForStreamItemPhotos(qb); 2662 Cursor c = qb.query(mDb, new String[]{StreamItemPhotos._ID, StreamItems.RAW_CONTACT_ID}, 2663 selection, selectionArgs, null, null, null); 2664 try { 2665 while (c.moveToNext()) { 2666 streamItemPhotoIds.add(c.getLong(0)); 2667 2668 // Throw a security exception if the account doesn't match the raw contact's. 2669 enforceModifyingAccount(account, c.getLong(1)); 2670 } 2671 } finally { 2672 c.close(); 2673 } 2674 return streamItemPhotoIds; 2675 } 2676 2677 /** 2678 * Queries the database for stream items under the given raw contact. If there are 2679 * more entries than {@link ContactsProvider2#MAX_STREAM_ITEMS_PER_RAW_CONTACT}, 2680 * the oldest entries (as determined by timestamp) will be deleted. 2681 * @param rawContactId The raw contact ID to examine for stream items. 2682 * @param insertedStreamItemId The ID of the stream item that was just inserted, 2683 * prompting this cleanup. Callers may pass 0 if no insertion prompted the 2684 * cleanup. 2685 * @return The ID of the inserted stream item if it still exists after cleanup; 2686 * 0 otherwise. 2687 */ 2688 private long cleanUpOldStreamItems(long rawContactId, long insertedStreamItemId) { 2689 long postCleanupInsertedStreamId = insertedStreamItemId; 2690 Cursor c = mDb.query(Tables.STREAM_ITEMS, new String[]{StreamItems._ID}, 2691 StreamItems.RAW_CONTACT_ID + "=?", new String[]{String.valueOf(rawContactId)}, 2692 null, null, StreamItems.TIMESTAMP + " DESC, " + StreamItems._ID + " DESC"); 2693 try { 2694 int streamItemCount = c.getCount(); 2695 if (streamItemCount <= MAX_STREAM_ITEMS_PER_RAW_CONTACT) { 2696 // Still under the limit - nothing to clean up! 2697 return insertedStreamItemId; 2698 } else { 2699 c.moveToLast(); 2700 while (c.getPosition() >= MAX_STREAM_ITEMS_PER_RAW_CONTACT) { 2701 long streamItemId = c.getLong(0); 2702 if (insertedStreamItemId == streamItemId) { 2703 // The stream item just inserted is being deleted. 2704 postCleanupInsertedStreamId = 0; 2705 } 2706 deleteStreamItem(c.getLong(0)); 2707 c.moveToPrevious(); 2708 } 2709 } 2710 } finally { 2711 c.close(); 2712 } 2713 return postCleanupInsertedStreamId; 2714 } 2715 2716 public void updateRawContactDisplayName(SQLiteDatabase db, long rawContactId) { 2717 mDbHelper.updateRawContactDisplayName(db, rawContactId); 2718 } 2719 2720 /** 2721 * Delete data row by row so that fixing of primaries etc work correctly. 2722 */ 2723 private int deleteData(String selection, String[] selectionArgs, boolean callerIsSyncAdapter) { 2724 int count = 0; 2725 2726 // Note that the query will return data according to the access restrictions, 2727 // so we don't need to worry about deleting data we don't have permission to read. 2728 Cursor c = query(Data.CONTENT_URI, DataRowHandler.DataDeleteQuery.COLUMNS, 2729 selection, selectionArgs, null); 2730 try { 2731 while(c.moveToNext()) { 2732 long rawContactId = c.getLong(DataRowHandler.DataDeleteQuery.RAW_CONTACT_ID); 2733 2734 // Check for write profile permission if the data belongs to the profile. 2735 enforceProfilePermissionForRawContact(mDb, rawContactId, true); 2736 2737 String mimeType = c.getString(DataRowHandler.DataDeleteQuery.MIMETYPE); 2738 DataRowHandler rowHandler = getDataRowHandler(mimeType); 2739 count += rowHandler.delete(mDb, mTransactionContext, c); 2740 if (!callerIsSyncAdapter) { 2741 mTransactionContext.markRawContactDirty(rawContactId); 2742 } 2743 } 2744 } finally { 2745 c.close(); 2746 } 2747 2748 return count; 2749 } 2750 2751 /** 2752 * Delete a data row provided that it is one of the allowed mime types. 2753 */ 2754 public int deleteData(long dataId, String[] allowedMimeTypes) { 2755 2756 // Note that the query will return data according to the access restrictions, 2757 // so we don't need to worry about deleting data we don't have permission to read. 2758 mSelectionArgs1[0] = String.valueOf(dataId); 2759 Cursor c = query(Data.CONTENT_URI, DataRowHandler.DataDeleteQuery.COLUMNS, Data._ID + "=?", 2760 mSelectionArgs1, null); 2761 2762 try { 2763 if (!c.moveToFirst()) { 2764 return 0; 2765 } 2766 2767 String mimeType = c.getString(DataRowHandler.DataDeleteQuery.MIMETYPE); 2768 boolean valid = false; 2769 for (int i = 0; i < allowedMimeTypes.length; i++) { 2770 if (TextUtils.equals(mimeType, allowedMimeTypes[i])) { 2771 valid = true; 2772 break; 2773 } 2774 } 2775 2776 if (!valid) { 2777 throw new IllegalArgumentException("Data type mismatch: expected " 2778 + Lists.newArrayList(allowedMimeTypes)); 2779 } 2780 2781 // Check for write profile permission if the data belongs to the profile. 2782 long rawContactId = c.getLong(DataRowHandler.DataDeleteQuery.RAW_CONTACT_ID); 2783 enforceProfilePermissionForRawContact(mDb, rawContactId, true); 2784 2785 DataRowHandler rowHandler = getDataRowHandler(mimeType); 2786 return rowHandler.delete(mDb, mTransactionContext, c); 2787 } finally { 2788 c.close(); 2789 } 2790 } 2791 2792 /** 2793 * Inserts an item in the groups table 2794 */ 2795 private long insertGroup(Uri uri, ContentValues values, boolean callerIsSyncAdapter) { 2796 mValues.clear(); 2797 mValues.putAll(values); 2798 2799 final Account account = resolveAccount(uri, mValues); 2800 String dataSet = null; 2801 if (account != null && mValues.containsKey(Groups.DATA_SET)) { 2802 dataSet = mValues.getAsString(Groups.DATA_SET); 2803 } 2804 2805 // Replace package with internal mapping 2806 final String packageName = mValues.getAsString(Groups.RES_PACKAGE); 2807 if (packageName != null) { 2808 mValues.put(GroupsColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName)); 2809 } 2810 mValues.remove(Groups.RES_PACKAGE); 2811 2812 final boolean isFavoritesGroup = mValues.getAsLong(Groups.FAVORITES) != null 2813 ? mValues.getAsLong(Groups.FAVORITES) != 0 2814 : false; 2815 2816 if (!callerIsSyncAdapter) { 2817 mValues.put(Groups.DIRTY, 1); 2818 } 2819 2820 long result = mDb.insert(Tables.GROUPS, Groups.TITLE, mValues); 2821 2822 if (!callerIsSyncAdapter && isFavoritesGroup) { 2823 // add all starred raw contacts to this group 2824 String selection; 2825 String[] selectionArgs; 2826 if (account == null) { 2827 selection = RawContacts.ACCOUNT_NAME + " IS NULL AND " 2828 + RawContacts.ACCOUNT_TYPE + " IS NULL AND " 2829 + RawContacts.DATA_SET + " IS NULL"; 2830 selectionArgs = null; 2831 } else if (dataSet == null) { 2832 selection = RawContacts.ACCOUNT_NAME + "=? AND " 2833 + RawContacts.ACCOUNT_TYPE + "=?"; 2834 selectionArgs = new String[]{account.name, account.type}; 2835 } else { 2836 selection = RawContacts.ACCOUNT_NAME + "=? AND " 2837 + RawContacts.ACCOUNT_TYPE + "=? AND " 2838 + RawContacts.DATA_SET + "=?"; 2839 selectionArgs = new String[]{account.name, account.type, dataSet}; 2840 } 2841 Cursor c = mDb.query(Tables.RAW_CONTACTS, 2842 new String[]{RawContacts._ID, RawContacts.STARRED}, 2843 selection, selectionArgs, null, null, null); 2844 try { 2845 while (c.moveToNext()) { 2846 if (c.getLong(1) != 0) { 2847 final long rawContactId = c.getLong(0); 2848 insertDataGroupMembership(rawContactId, result); 2849 mTransactionContext.markRawContactDirty(rawContactId); 2850 } 2851 } 2852 } finally { 2853 c.close(); 2854 } 2855 } 2856 2857 if (mValues.containsKey(Groups.GROUP_VISIBLE)) { 2858 mVisibleTouched = true; 2859 } 2860 2861 return result; 2862 } 2863 2864 private long insertSettings(Uri uri, ContentValues values) { 2865 final long id = mDb.insert(Tables.SETTINGS, null, values); 2866 2867 if (values.containsKey(Settings.UNGROUPED_VISIBLE)) { 2868 mVisibleTouched = true; 2869 } 2870 2871 return id; 2872 } 2873 2874 /** 2875 * Inserts a status update. 2876 */ 2877 public long insertStatusUpdate(ContentValues values) { 2878 final String handle = values.getAsString(StatusUpdates.IM_HANDLE); 2879 final Integer protocol = values.getAsInteger(StatusUpdates.PROTOCOL); 2880 String customProtocol = null; 2881 2882 if (protocol != null && protocol == Im.PROTOCOL_CUSTOM) { 2883 customProtocol = values.getAsString(StatusUpdates.CUSTOM_PROTOCOL); 2884 if (TextUtils.isEmpty(customProtocol)) { 2885 throw new IllegalArgumentException( 2886 "CUSTOM_PROTOCOL is required when PROTOCOL=PROTOCOL_CUSTOM"); 2887 } 2888 } 2889 2890 long rawContactId = -1; 2891 long contactId = -1; 2892 Long dataId = values.getAsLong(StatusUpdates.DATA_ID); 2893 String accountType = null; 2894 String accountName = null; 2895 mSb.setLength(0); 2896 mSelectionArgs.clear(); 2897 if (dataId != null) { 2898 // Lookup the contact info for the given data row. 2899 2900 mSb.append(Tables.DATA + "." + Data._ID + "=?"); 2901 mSelectionArgs.add(String.valueOf(dataId)); 2902 } else { 2903 // Lookup the data row to attach this presence update to 2904 2905 if (TextUtils.isEmpty(handle) || protocol == null) { 2906 throw new IllegalArgumentException("PROTOCOL and IM_HANDLE are required"); 2907 } 2908 2909 // TODO: generalize to allow other providers to match against email 2910 boolean matchEmail = Im.PROTOCOL_GOOGLE_TALK == protocol; 2911 2912 String mimeTypeIdIm = String.valueOf(mDbHelper.getMimeTypeIdForIm()); 2913 if (matchEmail) { 2914 String mimeTypeIdEmail = String.valueOf(mDbHelper.getMimeTypeIdForEmail()); 2915 2916 // The following hack forces SQLite to use the (mimetype_id,data1) index, otherwise 2917 // the "OR" conjunction confuses it and it switches to a full scan of 2918 // the raw_contacts table. 2919 2920 // This code relies on the fact that Im.DATA and Email.DATA are in fact the same 2921 // column - Data.DATA1 2922 mSb.append(DataColumns.MIMETYPE_ID + " IN (?,?)" + 2923 " AND " + Data.DATA1 + "=?" + 2924 " AND ((" + DataColumns.MIMETYPE_ID + "=? AND " + Im.PROTOCOL + "=?"); 2925 mSelectionArgs.add(mimeTypeIdEmail); 2926 mSelectionArgs.add(mimeTypeIdIm); 2927 mSelectionArgs.add(handle); 2928 mSelectionArgs.add(mimeTypeIdIm); 2929 mSelectionArgs.add(String.valueOf(protocol)); 2930 if (customProtocol != null) { 2931 mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?"); 2932 mSelectionArgs.add(customProtocol); 2933 } 2934 mSb.append(") OR (" + DataColumns.MIMETYPE_ID + "=?))"); 2935 mSelectionArgs.add(mimeTypeIdEmail); 2936 } else { 2937 mSb.append(DataColumns.MIMETYPE_ID + "=?" + 2938 " AND " + Im.PROTOCOL + "=?" + 2939 " AND " + Im.DATA + "=?"); 2940 mSelectionArgs.add(mimeTypeIdIm); 2941 mSelectionArgs.add(String.valueOf(protocol)); 2942 mSelectionArgs.add(handle); 2943 if (customProtocol != null) { 2944 mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?"); 2945 mSelectionArgs.add(customProtocol); 2946 } 2947 } 2948 2949 if (values.containsKey(StatusUpdates.DATA_ID)) { 2950 mSb.append(" AND " + DataColumns.CONCRETE_ID + "=?"); 2951 mSelectionArgs.add(values.getAsString(StatusUpdates.DATA_ID)); 2952 } 2953 } 2954 2955 Cursor cursor = null; 2956 try { 2957 cursor = mDb.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION, 2958 mSb.toString(), mSelectionArgs.toArray(EMPTY_STRING_ARRAY), null, null, 2959 Clauses.CONTACT_VISIBLE + " DESC, " + Data.RAW_CONTACT_ID); 2960 if (cursor.moveToFirst()) { 2961 dataId = cursor.getLong(DataContactsQuery.DATA_ID); 2962 rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID); 2963 accountType = cursor.getString(DataContactsQuery.ACCOUNT_TYPE); 2964 accountName = cursor.getString(DataContactsQuery.ACCOUNT_NAME); 2965 contactId = cursor.getLong(DataContactsQuery.CONTACT_ID); 2966 } else { 2967 // No contact found, return a null URI 2968 return -1; 2969 } 2970 } finally { 2971 if (cursor != null) { 2972 cursor.close(); 2973 } 2974 } 2975 2976 if (values.containsKey(StatusUpdates.PRESENCE)) { 2977 if (customProtocol == null) { 2978 // We cannot allow a null in the custom protocol field, because SQLite3 does not 2979 // properly enforce uniqueness of null values 2980 customProtocol = ""; 2981 } 2982 2983 mValues.clear(); 2984 mValues.put(StatusUpdates.DATA_ID, dataId); 2985 mValues.put(PresenceColumns.RAW_CONTACT_ID, rawContactId); 2986 mValues.put(PresenceColumns.CONTACT_ID, contactId); 2987 mValues.put(StatusUpdates.PROTOCOL, protocol); 2988 mValues.put(StatusUpdates.CUSTOM_PROTOCOL, customProtocol); 2989 mValues.put(StatusUpdates.IM_HANDLE, handle); 2990 if (values.containsKey(StatusUpdates.IM_ACCOUNT)) { 2991 mValues.put(StatusUpdates.IM_ACCOUNT, values.getAsString(StatusUpdates.IM_ACCOUNT)); 2992 } 2993 mValues.put(StatusUpdates.PRESENCE, 2994 values.getAsString(StatusUpdates.PRESENCE)); 2995 mValues.put(StatusUpdates.CHAT_CAPABILITY, 2996 values.getAsString(StatusUpdates.CHAT_CAPABILITY)); 2997 2998 // Insert the presence update 2999 mDb.replace(Tables.PRESENCE, null, mValues); 3000 } 3001 3002 3003 if (values.containsKey(StatusUpdates.STATUS)) { 3004 String status = values.getAsString(StatusUpdates.STATUS); 3005 String resPackage = values.getAsString(StatusUpdates.STATUS_RES_PACKAGE); 3006 Integer labelResource = values.getAsInteger(StatusUpdates.STATUS_LABEL); 3007 3008 if (TextUtils.isEmpty(resPackage) 3009 && (labelResource == null || labelResource == 0) 3010 && protocol != null) { 3011 labelResource = Im.getProtocolLabelResource(protocol); 3012 } 3013 3014 Long iconResource = values.getAsLong(StatusUpdates.STATUS_ICON); 3015 // TODO compute the default icon based on the protocol 3016 3017 if (TextUtils.isEmpty(status)) { 3018 mDbHelper.deleteStatusUpdate(dataId); 3019 } else { 3020 Long timestamp = values.getAsLong(StatusUpdates.STATUS_TIMESTAMP); 3021 if (timestamp != null) { 3022 mDbHelper.replaceStatusUpdate(dataId, timestamp, status, resPackage, 3023 iconResource, labelResource); 3024 } else { 3025 mDbHelper.insertStatusUpdate(dataId, status, resPackage, iconResource, 3026 labelResource); 3027 } 3028 3029 // For forward compatibility with the new stream item API, insert this status update 3030 // there as well. If we already have a stream item from this source, update that 3031 // one instead of inserting a new one (since the semantics of the old status update 3032 // API is to only have a single record). 3033 if (rawContactId != -1 && !TextUtils.isEmpty(status)) { 3034 ContentValues streamItemValues = new ContentValues(); 3035 streamItemValues.put(StreamItems.RAW_CONTACT_ID, rawContactId); 3036 streamItemValues.put(StreamItems.TEXT, status); 3037 streamItemValues.put(StreamItems.COMMENTS, ""); 3038 streamItemValues.put(StreamItems.RES_PACKAGE, resPackage); 3039 streamItemValues.put(StreamItems.RES_ICON, iconResource); 3040 streamItemValues.put(StreamItems.RES_LABEL, labelResource); 3041 streamItemValues.put(StreamItems.TIMESTAMP, 3042 timestamp == null ? System.currentTimeMillis() : timestamp); 3043 3044 // Note: The following is basically a workaround for the fact that status 3045 // updates didn't do any sort of account enforcement, while social stream item 3046 // updates do. We can't expect callers of the old API to start passing account 3047 // information along, so we just populate the account params appropriately for 3048 // the raw contact. Data set is not relevant here, as we only check account 3049 // name and type. 3050 if (accountName != null && accountType != null) { 3051 streamItemValues.put(RawContacts.ACCOUNT_NAME, accountName); 3052 streamItemValues.put(RawContacts.ACCOUNT_TYPE, accountType); 3053 } 3054 3055 // Check for an existing stream item from this source, and insert or update. 3056 Uri streamUri = StreamItems.CONTENT_URI; 3057 Cursor c = query(streamUri, new String[]{StreamItems._ID}, 3058 StreamItems.RAW_CONTACT_ID + "=?", 3059 new String[]{String.valueOf(rawContactId)}, null); 3060 try { 3061 if (c.getCount() > 0) { 3062 c.moveToFirst(); 3063 update(ContentUris.withAppendedId(streamUri, c.getLong(0)), 3064 streamItemValues, null, null); 3065 } else { 3066 insert(streamUri, streamItemValues); 3067 } 3068 } finally { 3069 c.close(); 3070 } 3071 3072 } 3073 } 3074 } 3075 3076 if (contactId != -1) { 3077 mContactAggregator.updateLastStatusUpdateId(contactId); 3078 } 3079 3080 return dataId; 3081 } 3082 3083 @Override 3084 protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) { 3085 if (VERBOSE_LOGGING) { 3086 Log.v(TAG, "deleteInTransaction: " + uri); 3087 } 3088 flushTransactionalChanges(); 3089 final boolean callerIsSyncAdapter = 3090 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); 3091 final int match = sUriMatcher.match(uri); 3092 switch (match) { 3093 case SYNCSTATE: 3094 return mDbHelper.getSyncState().delete(mDb, selection, selectionArgs); 3095 3096 case SYNCSTATE_ID: 3097 String selectionWithId = 3098 (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") 3099 + (selection == null ? "" : " AND (" + selection + ")"); 3100 return mDbHelper.getSyncState().delete(mDb, selectionWithId, selectionArgs); 3101 3102 case CONTACTS: { 3103 // TODO 3104 return 0; 3105 } 3106 3107 case CONTACTS_ID: { 3108 long contactId = ContentUris.parseId(uri); 3109 return deleteContact(contactId, callerIsSyncAdapter); 3110 } 3111 3112 case CONTACTS_LOOKUP: { 3113 final List<String> pathSegments = uri.getPathSegments(); 3114 final int segmentCount = pathSegments.size(); 3115 if (segmentCount < 3) { 3116 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 3117 "Missing a lookup key", uri)); 3118 } 3119 final String lookupKey = pathSegments.get(2); 3120 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey); 3121 return deleteContact(contactId, callerIsSyncAdapter); 3122 } 3123 3124 case CONTACTS_LOOKUP_ID: { 3125 // lookup contact by id and lookup key to see if they still match the actual record 3126 final List<String> pathSegments = uri.getPathSegments(); 3127 final String lookupKey = pathSegments.get(2); 3128 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 3129 setTablesAndProjectionMapForContacts(lookupQb, uri, null); 3130 long contactId = ContentUris.parseId(uri); 3131 String[] args; 3132 if (selectionArgs == null) { 3133 args = new String[2]; 3134 } else { 3135 args = new String[selectionArgs.length + 2]; 3136 System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length); 3137 } 3138 args[0] = String.valueOf(contactId); 3139 args[1] = Uri.encode(lookupKey); 3140 lookupQb.appendWhere(Contacts._ID + "=? AND " + Contacts.LOOKUP_KEY + "=?"); 3141 final SQLiteDatabase db = mDbHelper.getReadableDatabase(); 3142 Cursor c = query(db, lookupQb, null, selection, args, null, null, null); 3143 try { 3144 if (c.getCount() == 1) { 3145 // contact was unmodified so go ahead and delete it 3146 return deleteContact(contactId, callerIsSyncAdapter); 3147 } else { 3148 // row was changed (e.g. the merging might have changed), we got multiple 3149 // rows or the supplied selection filtered the record out 3150 return 0; 3151 } 3152 } finally { 3153 c.close(); 3154 } 3155 } 3156 3157 case RAW_CONTACTS: { 3158 int numDeletes = 0; 3159 Cursor c = mDb.query(Tables.RAW_CONTACTS, 3160 new String[]{RawContacts._ID, RawContacts.CONTACT_ID}, 3161 appendAccountToSelection(uri, selection), selectionArgs, null, null, null); 3162 try { 3163 while (c.moveToNext()) { 3164 final long rawContactId = c.getLong(0); 3165 long contactId = c.getLong(1); 3166 numDeletes += deleteRawContact(rawContactId, contactId, 3167 callerIsSyncAdapter); 3168 } 3169 } finally { 3170 c.close(); 3171 } 3172 return numDeletes; 3173 } 3174 3175 case RAW_CONTACTS_ID: { 3176 final long rawContactId = ContentUris.parseId(uri); 3177 return deleteRawContact(rawContactId, mDbHelper.getContactId(rawContactId), 3178 callerIsSyncAdapter); 3179 } 3180 3181 case DATA: { 3182 mSyncToNetwork |= !callerIsSyncAdapter; 3183 return deleteData(appendAccountToSelection(uri, selection), selectionArgs, 3184 callerIsSyncAdapter); 3185 } 3186 3187 case DATA_ID: 3188 case PHONES_ID: 3189 case EMAILS_ID: 3190 case POSTALS_ID: { 3191 long dataId = ContentUris.parseId(uri); 3192 mSyncToNetwork |= !callerIsSyncAdapter; 3193 mSelectionArgs1[0] = String.valueOf(dataId); 3194 return deleteData(Data._ID + "=?", mSelectionArgs1, callerIsSyncAdapter); 3195 } 3196 3197 case GROUPS_ID: { 3198 mSyncToNetwork |= !callerIsSyncAdapter; 3199 return deleteGroup(uri, ContentUris.parseId(uri), callerIsSyncAdapter); 3200 } 3201 3202 case GROUPS: { 3203 int numDeletes = 0; 3204 Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups._ID}, 3205 appendAccountToSelection(uri, selection), selectionArgs, null, null, null); 3206 try { 3207 while (c.moveToNext()) { 3208 numDeletes += deleteGroup(uri, c.getLong(0), callerIsSyncAdapter); 3209 } 3210 } finally { 3211 c.close(); 3212 } 3213 if (numDeletes > 0) { 3214 mSyncToNetwork |= !callerIsSyncAdapter; 3215 } 3216 return numDeletes; 3217 } 3218 3219 case SETTINGS: { 3220 mSyncToNetwork |= !callerIsSyncAdapter; 3221 return deleteSettings(uri, appendAccountToSelection(uri, selection), selectionArgs); 3222 } 3223 3224 case STATUS_UPDATES: { 3225 return deleteStatusUpdates(selection, selectionArgs); 3226 } 3227 3228 case STREAM_ITEMS: { 3229 mSyncToNetwork |= !callerIsSyncAdapter; 3230 return deleteStreamItems(uri, new ContentValues(), selection, selectionArgs); 3231 } 3232 3233 case STREAM_ITEMS_ID: { 3234 mSyncToNetwork |= !callerIsSyncAdapter; 3235 return deleteStreamItems(uri, new ContentValues(), 3236 StreamItemsColumns.CONCRETE_ID + "=?", 3237 new String[]{uri.getLastPathSegment()}); 3238 } 3239 3240 case STREAM_ITEMS_ID_PHOTOS: { 3241 mSyncToNetwork |= !callerIsSyncAdapter; 3242 return deleteStreamItemPhotos(uri, new ContentValues(), selection, selectionArgs); 3243 } 3244 3245 case STREAM_ITEMS_ID_PHOTOS_ID: { 3246 mSyncToNetwork |= !callerIsSyncAdapter; 3247 String streamItemId = uri.getPathSegments().get(1); 3248 String streamItemPhotoId = uri.getPathSegments().get(3); 3249 return deleteStreamItemPhotos(uri, new ContentValues(), 3250 StreamItemPhotosColumns.CONCRETE_ID + "=? AND " 3251 + StreamItemPhotos.STREAM_ITEM_ID + "=?", 3252 new String[]{streamItemPhotoId, streamItemId}); 3253 } 3254 3255 default: { 3256 mSyncToNetwork = true; 3257 return mLegacyApiSupport.delete(uri, selection, selectionArgs); 3258 } 3259 } 3260 } 3261 3262 public int deleteGroup(Uri uri, long groupId, boolean callerIsSyncAdapter) { 3263 mGroupIdCache.clear(); 3264 final long groupMembershipMimetypeId = mDbHelper 3265 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE); 3266 mDb.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "=" 3267 + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "=" 3268 + groupId, null); 3269 3270 try { 3271 if (callerIsSyncAdapter) { 3272 return mDb.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null); 3273 } else { 3274 mValues.clear(); 3275 mValues.put(Groups.DELETED, 1); 3276 mValues.put(Groups.DIRTY, 1); 3277 return mDb.update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId, null); 3278 } 3279 } finally { 3280 mVisibleTouched = true; 3281 } 3282 } 3283 3284 private int deleteSettings(Uri uri, String selection, String[] selectionArgs) { 3285 final int count = mDb.delete(Tables.SETTINGS, selection, selectionArgs); 3286 mVisibleTouched = true; 3287 return count; 3288 } 3289 3290 private int deleteContact(long contactId, boolean callerIsSyncAdapter) { 3291 enforceProfilePermissionForContact(mDb, contactId, true); 3292 mSelectionArgs1[0] = Long.toString(contactId); 3293 Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID}, 3294 RawContacts.CONTACT_ID + "=?", mSelectionArgs1, 3295 null, null, null); 3296 try { 3297 while (c.moveToNext()) { 3298 long rawContactId = c.getLong(0); 3299 markRawContactAsDeleted(rawContactId, callerIsSyncAdapter); 3300 } 3301 } finally { 3302 c.close(); 3303 } 3304 3305 mProviderStatusUpdateNeeded = true; 3306 3307 return mDb.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null); 3308 } 3309 3310 public int deleteRawContact(long rawContactId, long contactId, boolean callerIsSyncAdapter) { 3311 enforceProfilePermissionForRawContact(mDb, rawContactId, true); 3312 mContactAggregator.invalidateAggregationExceptionCache(); 3313 mProviderStatusUpdateNeeded = true; 3314 3315 if (callerIsSyncAdapter) { 3316 mDb.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null); 3317 int count = mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null); 3318 mContactAggregator.updateDisplayNameForContact(mDb, contactId); 3319 return count; 3320 } else { 3321 mDbHelper.removeContactIfSingleton(rawContactId); 3322 return markRawContactAsDeleted(rawContactId, callerIsSyncAdapter); 3323 } 3324 } 3325 3326 private int deleteStatusUpdates(String selection, String[] selectionArgs) { 3327 // delete from both tables: presence and status_updates 3328 // TODO should account type/name be appended to the where clause? 3329 if (VERBOSE_LOGGING) { 3330 Log.v(TAG, "deleting data from status_updates for " + selection); 3331 } 3332 mDb.delete(Tables.STATUS_UPDATES, getWhereClauseForStatusUpdatesTable(selection), 3333 selectionArgs); 3334 return mDb.delete(Tables.PRESENCE, selection, selectionArgs); 3335 } 3336 3337 private int deleteStreamItems(Uri uri, ContentValues values, String selection, 3338 String[] selectionArgs) { 3339 // First query for the stream items to be deleted, and check that they belong 3340 // to the account. 3341 Account account = resolveAccount(uri, values); 3342 List<Long> streamItemIds = enforceModifyingAccountForStreamItems( 3343 account, selection, selectionArgs); 3344 3345 // If no security exception has been thrown, we're fine to delete. 3346 for (long streamItemId : streamItemIds) { 3347 deleteStreamItem(streamItemId); 3348 } 3349 3350 mVisibleTouched = true; 3351 return streamItemIds.size(); 3352 } 3353 3354 private int deleteStreamItem(long streamItemId) { 3355 // Note that this does not enforce the modifying account. 3356 deleteStreamItemPhotos(streamItemId); 3357 return mDb.delete(Tables.STREAM_ITEMS, StreamItems._ID + "=?", 3358 new String[]{String.valueOf(streamItemId)}); 3359 } 3360 3361 private int deleteStreamItemPhotos(Uri uri, ContentValues values, String selection, 3362 String[] selectionArgs) { 3363 // First query for the stream item photos to be deleted, and check that they 3364 // belong to the account. 3365 Account account = resolveAccount(uri, values); 3366 enforceModifyingAccountForStreamItemPhotos(account, selection, selectionArgs); 3367 3368 // If no security exception has been thrown, we're fine to delete. 3369 return mDb.delete(Tables.STREAM_ITEM_PHOTOS, selection, selectionArgs); 3370 } 3371 3372 private int deleteStreamItemPhotos(long streamItemId) { 3373 // Note that this does not enforce the modifying account. 3374 return mDb.delete(Tables.STREAM_ITEM_PHOTOS, StreamItemPhotos.STREAM_ITEM_ID + "=?", 3375 new String[]{String.valueOf(streamItemId)}); 3376 } 3377 3378 private int markRawContactAsDeleted(long rawContactId, boolean callerIsSyncAdapter) { 3379 mSyncToNetwork = true; 3380 3381 mValues.clear(); 3382 mValues.put(RawContacts.DELETED, 1); 3383 mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED); 3384 mValues.put(RawContactsColumns.AGGREGATION_NEEDED, 1); 3385 mValues.putNull(RawContacts.CONTACT_ID); 3386 mValues.put(RawContacts.DIRTY, 1); 3387 return updateRawContact(rawContactId, mValues, callerIsSyncAdapter); 3388 } 3389 3390 @Override 3391 protected int updateInTransaction(Uri uri, ContentValues values, String selection, 3392 String[] selectionArgs) { 3393 if (VERBOSE_LOGGING) { 3394 Log.v(TAG, "updateInTransaction: " + uri); 3395 } 3396 3397 int count = 0; 3398 3399 final int match = sUriMatcher.match(uri); 3400 if (match == SYNCSTATE_ID && selection == null) { 3401 long rowId = ContentUris.parseId(uri); 3402 Object data = values.get(ContactsContract.SyncState.DATA); 3403 mTransactionContext.syncStateUpdated(rowId, data); 3404 return 1; 3405 } 3406 flushTransactionalChanges(); 3407 final boolean callerIsSyncAdapter = 3408 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); 3409 switch(match) { 3410 case SYNCSTATE: 3411 return mDbHelper.getSyncState().update(mDb, values, 3412 appendAccountToSelection(uri, selection), selectionArgs); 3413 3414 case SYNCSTATE_ID: { 3415 selection = appendAccountToSelection(uri, selection); 3416 String selectionWithId = 3417 (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") 3418 + (selection == null ? "" : " AND (" + selection + ")"); 3419 return mDbHelper.getSyncState().update(mDb, values, 3420 selectionWithId, selectionArgs); 3421 } 3422 3423 case CONTACTS: { 3424 count = updateContactOptions(values, selection, selectionArgs, callerIsSyncAdapter); 3425 break; 3426 } 3427 3428 case CONTACTS_ID: { 3429 count = updateContactOptions(ContentUris.parseId(uri), values, callerIsSyncAdapter); 3430 break; 3431 } 3432 3433 case PROFILE: { 3434 // Restrict update to the user's profile. 3435 StringBuilder profileSelection = new StringBuilder(); 3436 profileSelection.append(Contacts.IS_USER_PROFILE + "=1"); 3437 if (!TextUtils.isEmpty(selection)) { 3438 profileSelection.append(" AND (").append(selection).append(")"); 3439 } 3440 count = updateContactOptions(values, profileSelection.toString(), selectionArgs, 3441 callerIsSyncAdapter); 3442 break; 3443 } 3444 3445 case CONTACTS_LOOKUP: 3446 case CONTACTS_LOOKUP_ID: { 3447 final List<String> pathSegments = uri.getPathSegments(); 3448 final int segmentCount = pathSegments.size(); 3449 if (segmentCount < 3) { 3450 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 3451 "Missing a lookup key", uri)); 3452 } 3453 final String lookupKey = pathSegments.get(2); 3454 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey); 3455 count = updateContactOptions(contactId, values, callerIsSyncAdapter); 3456 break; 3457 } 3458 3459 case RAW_CONTACTS_DATA: { 3460 final String rawContactId = uri.getPathSegments().get(1); 3461 String selectionWithId = (Data.RAW_CONTACT_ID + "=" + rawContactId + " ") 3462 + (selection == null ? "" : " AND " + selection); 3463 3464 count = updateData(uri, values, selectionWithId, selectionArgs, callerIsSyncAdapter); 3465 3466 break; 3467 } 3468 3469 case DATA: { 3470 count = updateData(uri, values, appendAccountToSelection(uri, selection), 3471 selectionArgs, callerIsSyncAdapter); 3472 if (count > 0) { 3473 mSyncToNetwork |= !callerIsSyncAdapter; 3474 } 3475 break; 3476 } 3477 3478 case DATA_ID: 3479 case PHONES_ID: 3480 case EMAILS_ID: 3481 case POSTALS_ID: { 3482 count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter); 3483 if (count > 0) { 3484 mSyncToNetwork |= !callerIsSyncAdapter; 3485 } 3486 break; 3487 } 3488 3489 case RAW_CONTACTS: { 3490 selection = appendAccountToSelection(uri, selection); 3491 count = updateRawContacts(values, selection, selectionArgs, callerIsSyncAdapter); 3492 break; 3493 } 3494 3495 case RAW_CONTACTS_ID: { 3496 long rawContactId = ContentUris.parseId(uri); 3497 if (selection != null) { 3498 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 3499 count = updateRawContacts(values, RawContacts._ID + "=?" 3500 + " AND(" + selection + ")", selectionArgs, 3501 callerIsSyncAdapter); 3502 } else { 3503 mSelectionArgs1[0] = String.valueOf(rawContactId); 3504 count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1, 3505 callerIsSyncAdapter); 3506 } 3507 break; 3508 } 3509 3510 case GROUPS: { 3511 count = updateGroups(uri, values, appendAccountToSelection(uri, selection), 3512 selectionArgs, callerIsSyncAdapter); 3513 if (count > 0) { 3514 mSyncToNetwork |= !callerIsSyncAdapter; 3515 } 3516 break; 3517 } 3518 3519 case GROUPS_ID: { 3520 long groupId = ContentUris.parseId(uri); 3521 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(groupId)); 3522 String selectionWithId = Groups._ID + "=? " 3523 + (selection == null ? "" : " AND " + selection); 3524 count = updateGroups(uri, values, selectionWithId, selectionArgs, 3525 callerIsSyncAdapter); 3526 if (count > 0) { 3527 mSyncToNetwork |= !callerIsSyncAdapter; 3528 } 3529 break; 3530 } 3531 3532 case AGGREGATION_EXCEPTIONS: { 3533 count = updateAggregationException(mDb, values); 3534 break; 3535 } 3536 3537 case SETTINGS: { 3538 count = updateSettings(uri, values, appendAccountToSelection(uri, selection), 3539 selectionArgs); 3540 mSyncToNetwork |= !callerIsSyncAdapter; 3541 break; 3542 } 3543 3544 case STATUS_UPDATES: { 3545 count = updateStatusUpdate(uri, values, selection, selectionArgs); 3546 break; 3547 } 3548 3549 case STREAM_ITEMS: { 3550 count = updateStreamItems(uri, values, selection, selectionArgs); 3551 break; 3552 } 3553 3554 case STREAM_ITEMS_ID: { 3555 count = updateStreamItems(uri, values, StreamItemsColumns.CONCRETE_ID + "=?", 3556 new String[]{uri.getLastPathSegment()}); 3557 break; 3558 } 3559 3560 case STREAM_ITEMS_PHOTOS: { 3561 count = updateStreamItemPhotos(uri, values, selection, selectionArgs); 3562 break; 3563 } 3564 3565 case STREAM_ITEMS_ID_PHOTOS: { 3566 String streamItemId = uri.getPathSegments().get(1); 3567 count = updateStreamItemPhotos(uri, values, 3568 StreamItemPhotos.STREAM_ITEM_ID + "=?", new String[]{streamItemId}); 3569 break; 3570 } 3571 3572 case STREAM_ITEMS_ID_PHOTOS_ID: { 3573 String streamItemId = uri.getPathSegments().get(1); 3574 String streamItemPhotoId = uri.getPathSegments().get(3); 3575 count = updateStreamItemPhotos(uri, values, 3576 StreamItemPhotosColumns.CONCRETE_ID + "=? AND " + 3577 StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID + "=?", 3578 new String[]{streamItemPhotoId, streamItemId}); 3579 break; 3580 } 3581 3582 case DIRECTORIES: { 3583 mContactDirectoryManager.scanPackagesByUid(Binder.getCallingUid()); 3584 count = 1; 3585 break; 3586 } 3587 3588 case DATA_USAGE_FEEDBACK_ID: { 3589 if (handleDataUsageFeedback(uri)) { 3590 count = 1; 3591 } else { 3592 count = 0; 3593 } 3594 break; 3595 } 3596 3597 default: { 3598 mSyncToNetwork = true; 3599 return mLegacyApiSupport.update(uri, values, selection, selectionArgs); 3600 } 3601 } 3602 3603 return count; 3604 } 3605 3606 private int updateStatusUpdate(Uri uri, ContentValues values, String selection, 3607 String[] selectionArgs) { 3608 // update status_updates table, if status is provided 3609 // TODO should account type/name be appended to the where clause? 3610 int updateCount = 0; 3611 ContentValues settableValues = getSettableColumnsForStatusUpdatesTable(values); 3612 if (settableValues.size() > 0) { 3613 updateCount = mDb.update(Tables.STATUS_UPDATES, 3614 settableValues, 3615 getWhereClauseForStatusUpdatesTable(selection), 3616 selectionArgs); 3617 } 3618 3619 // now update the Presence table 3620 settableValues = getSettableColumnsForPresenceTable(values); 3621 if (settableValues.size() > 0) { 3622 updateCount = mDb.update(Tables.PRESENCE, settableValues, 3623 selection, selectionArgs); 3624 } 3625 // TODO updateCount is not entirely a valid count of updated rows because 2 tables could 3626 // potentially get updated in this method. 3627 return updateCount; 3628 } 3629 3630 private int updateStreamItems(Uri uri, ContentValues values, String selection, 3631 String[] selectionArgs) { 3632 // Stream items can't be moved to a new raw contact. 3633 values.remove(StreamItems.RAW_CONTACT_ID); 3634 3635 // Check that the stream items being updated belong to the account. 3636 Account account = resolveAccount(uri, values); 3637 enforceModifyingAccountForStreamItems(account, selection, selectionArgs); 3638 3639 // Don't attempt to update accounts params - they don't exist in the stream items table. 3640 values.remove(RawContacts.ACCOUNT_NAME); 3641 values.remove(RawContacts.ACCOUNT_TYPE); 3642 3643 // If there's been no exception, the update should be fine. 3644 return mDb.update(Tables.STREAM_ITEMS, values, selection, selectionArgs); 3645 } 3646 3647 private int updateStreamItemPhotos(Uri uri, ContentValues values, String selection, 3648 String[] selectionArgs) { 3649 // Stream item photos can't be moved to a new stream item. 3650 values.remove(StreamItemPhotos.STREAM_ITEM_ID); 3651 3652 // Check that the stream item photos being updated belong to the account. 3653 Account account = resolveAccount(uri, values); 3654 enforceModifyingAccountForStreamItemPhotos(account, selection, selectionArgs); 3655 3656 // Don't attempt to update accounts params - they don't exist in the stream item 3657 // photos table. 3658 values.remove(RawContacts.ACCOUNT_NAME); 3659 values.remove(RawContacts.ACCOUNT_TYPE); 3660 3661 // Process the photo (since we're updating, it's valid for the photo to not be present). 3662 if (processStreamItemPhoto(values, true)) { 3663 // If there's been no exception, the update should be fine. 3664 return mDb.update(Tables.STREAM_ITEM_PHOTOS, values, selection, selectionArgs); 3665 } 3666 return 0; 3667 } 3668 3669 /** 3670 * Build a where clause to select the rows to be updated in status_updates table. 3671 */ 3672 private String getWhereClauseForStatusUpdatesTable(String selection) { 3673 mSb.setLength(0); 3674 mSb.append(WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE); 3675 mSb.append(selection); 3676 mSb.append(")"); 3677 return mSb.toString(); 3678 } 3679 3680 private ContentValues getSettableColumnsForStatusUpdatesTable(ContentValues values) { 3681 mValues.clear(); 3682 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS, values, 3683 StatusUpdates.STATUS); 3684 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_TIMESTAMP, values, 3685 StatusUpdates.STATUS_TIMESTAMP); 3686 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_RES_PACKAGE, values, 3687 StatusUpdates.STATUS_RES_PACKAGE); 3688 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_LABEL, values, 3689 StatusUpdates.STATUS_LABEL); 3690 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_ICON, values, 3691 StatusUpdates.STATUS_ICON); 3692 return mValues; 3693 } 3694 3695 private ContentValues getSettableColumnsForPresenceTable(ContentValues values) { 3696 mValues.clear(); 3697 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.PRESENCE, values, 3698 StatusUpdates.PRESENCE); 3699 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.CHAT_CAPABILITY, values, 3700 StatusUpdates.CHAT_CAPABILITY); 3701 return mValues; 3702 } 3703 3704 private int updateGroups(Uri uri, ContentValues values, String selectionWithId, 3705 String[] selectionArgs, boolean callerIsSyncAdapter) { 3706 3707 mGroupIdCache.clear(); 3708 3709 ContentValues updatedValues; 3710 if (!callerIsSyncAdapter && !values.containsKey(Groups.DIRTY)) { 3711 updatedValues = mValues; 3712 updatedValues.clear(); 3713 updatedValues.putAll(values); 3714 updatedValues.put(Groups.DIRTY, 1); 3715 } else { 3716 updatedValues = values; 3717 } 3718 3719 int count = mDb.update(Tables.GROUPS, updatedValues, selectionWithId, selectionArgs); 3720 if (updatedValues.containsKey(Groups.GROUP_VISIBLE)) { 3721 mVisibleTouched = true; 3722 } 3723 3724 // TODO: This will not work for groups that have a data set specified, since the content 3725 // resolver will not be able to request a sync for the right source (unless it is updated 3726 // to key off account with data set). 3727 if (updatedValues.containsKey(Groups.SHOULD_SYNC) 3728 && updatedValues.getAsInteger(Groups.SHOULD_SYNC) != 0) { 3729 Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups.ACCOUNT_NAME, 3730 Groups.ACCOUNT_TYPE}, selectionWithId, selectionArgs, null, 3731 null, null); 3732 String accountName; 3733 String accountType; 3734 try { 3735 while (c.moveToNext()) { 3736 accountName = c.getString(0); 3737 accountType = c.getString(1); 3738 if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { 3739 Account account = new Account(accountName, accountType); 3740 ContentResolver.requestSync(account, ContactsContract.AUTHORITY, 3741 new Bundle()); 3742 break; 3743 } 3744 } 3745 } finally { 3746 c.close(); 3747 } 3748 } 3749 return count; 3750 } 3751 3752 private int updateSettings(Uri uri, ContentValues values, String selection, 3753 String[] selectionArgs) { 3754 final int count = mDb.update(Tables.SETTINGS, values, selection, selectionArgs); 3755 if (values.containsKey(Settings.UNGROUPED_VISIBLE)) { 3756 mVisibleTouched = true; 3757 } 3758 return count; 3759 } 3760 3761 private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs, 3762 boolean callerIsSyncAdapter) { 3763 if (values.containsKey(RawContacts.CONTACT_ID)) { 3764 throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " + 3765 "in content values. Contact IDs are assigned automatically"); 3766 } 3767 3768 if (!callerIsSyncAdapter) { 3769 selection = DatabaseUtils.concatenateWhere(selection, 3770 RawContacts.RAW_CONTACT_IS_READ_ONLY + "=0"); 3771 } 3772 3773 int count = 0; 3774 Cursor cursor = mDb.query(Views.RAW_CONTACTS, 3775 new String[] { RawContacts._ID }, selection, 3776 selectionArgs, null, null, null); 3777 try { 3778 while (cursor.moveToNext()) { 3779 long rawContactId = cursor.getLong(0); 3780 updateRawContact(rawContactId, values, callerIsSyncAdapter); 3781 count++; 3782 } 3783 } finally { 3784 cursor.close(); 3785 } 3786 3787 return count; 3788 } 3789 3790 private int updateRawContact(long rawContactId, ContentValues values, 3791 boolean callerIsSyncAdapter) { 3792 3793 // Enforce profile permissions if the raw contact is in the user's profile. 3794 enforceProfilePermissionForRawContact(mDb, rawContactId, true); 3795 3796 final String selection = RawContacts._ID + " = ?"; 3797 mSelectionArgs1[0] = Long.toString(rawContactId); 3798 final boolean requestUndoDelete = (values.containsKey(RawContacts.DELETED) 3799 && values.getAsInteger(RawContacts.DELETED) == 0); 3800 int previousDeleted = 0; 3801 String accountType = null; 3802 String accountName = null; 3803 String dataSet = null; 3804 if (requestUndoDelete) { 3805 Cursor cursor = mDb.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS, selection, 3806 mSelectionArgs1, null, null, null); 3807 try { 3808 if (cursor.moveToFirst()) { 3809 previousDeleted = cursor.getInt(RawContactsQuery.DELETED); 3810 accountType = cursor.getString(RawContactsQuery.ACCOUNT_TYPE); 3811 accountName = cursor.getString(RawContactsQuery.ACCOUNT_NAME); 3812 dataSet = cursor.getString(RawContactsQuery.DATA_SET); 3813 } 3814 } finally { 3815 cursor.close(); 3816 } 3817 values.put(ContactsContract.RawContacts.AGGREGATION_MODE, 3818 ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT); 3819 } 3820 3821 int count = mDb.update(Tables.RAW_CONTACTS, values, selection, mSelectionArgs1); 3822 if (count != 0) { 3823 if (values.containsKey(RawContacts.AGGREGATION_MODE)) { 3824 int aggregationMode = values.getAsInteger(RawContacts.AGGREGATION_MODE); 3825 3826 // As per ContactsContract documentation, changing aggregation mode 3827 // to DEFAULT should not trigger aggregation 3828 if (aggregationMode != RawContacts.AGGREGATION_MODE_DEFAULT) { 3829 mContactAggregator.markForAggregation(rawContactId, aggregationMode, false); 3830 } 3831 } 3832 if (values.containsKey(RawContacts.STARRED)) { 3833 if (!callerIsSyncAdapter) { 3834 updateFavoritesMembership(rawContactId, 3835 values.getAsLong(RawContacts.STARRED) != 0); 3836 } 3837 mContactAggregator.updateStarred(rawContactId); 3838 } else { 3839 // if this raw contact is being associated with an account, then update the 3840 // favorites group membership based on whether or not this contact is starred. 3841 // If it is starred, add a group membership, if one doesn't already exist 3842 // otherwise delete any matching group memberships. 3843 if (!callerIsSyncAdapter && values.containsKey(RawContacts.ACCOUNT_NAME)) { 3844 boolean starred = 0 != DatabaseUtils.longForQuery(mDb, 3845 SELECTION_STARRED_FROM_RAW_CONTACTS, 3846 new String[]{Long.toString(rawContactId)}); 3847 updateFavoritesMembership(rawContactId, starred); 3848 } 3849 } 3850 3851 // if this raw contact is being associated with an account, then add a 3852 // group membership to the group marked as AutoAdd, if any. 3853 if (!callerIsSyncAdapter && values.containsKey(RawContacts.ACCOUNT_NAME)) { 3854 addAutoAddMembership(rawContactId); 3855 } 3856 3857 if (values.containsKey(RawContacts.SOURCE_ID)) { 3858 mContactAggregator.updateLookupKeyForRawContact(mDb, rawContactId); 3859 } 3860 if (values.containsKey(RawContacts.NAME_VERIFIED)) { 3861 3862 // If setting NAME_VERIFIED for this raw contact, reset it for all 3863 // other raw contacts in the same aggregate 3864 if (values.getAsInteger(RawContacts.NAME_VERIFIED) != 0) { 3865 mDbHelper.resetNameVerifiedForOtherRawContacts(rawContactId); 3866 } 3867 mContactAggregator.updateDisplayNameForRawContact(mDb, rawContactId); 3868 } 3869 if (requestUndoDelete && previousDeleted == 1) { 3870 mTransactionContext.rawContactInserted(rawContactId, 3871 new AccountWithDataSet(accountName, accountType, dataSet)); 3872 } 3873 } 3874 return count; 3875 } 3876 3877 private int updateData(Uri uri, ContentValues values, String selection, 3878 String[] selectionArgs, boolean callerIsSyncAdapter) { 3879 mValues.clear(); 3880 mValues.putAll(values); 3881 mValues.remove(Data._ID); 3882 mValues.remove(Data.RAW_CONTACT_ID); 3883 mValues.remove(Data.MIMETYPE); 3884 3885 String packageName = values.getAsString(Data.RES_PACKAGE); 3886 if (packageName != null) { 3887 mValues.remove(Data.RES_PACKAGE); 3888 mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName)); 3889 } 3890 3891 if (!callerIsSyncAdapter) { 3892 selection = DatabaseUtils.concatenateWhere(selection, 3893 Data.IS_READ_ONLY + "=0"); 3894 } 3895 3896 int count = 0; 3897 3898 // Note that the query will return data according to the access restrictions, 3899 // so we don't need to worry about updating data we don't have permission to read. 3900 // This query will be allowed to return profiles, and we'll do the permission check 3901 // within the loop. 3902 Cursor c = queryLocal(uri.buildUpon() 3903 .appendQueryParameter(ContactsContract.ALLOW_PROFILE, "1").build(), 3904 DataRowHandler.DataUpdateQuery.COLUMNS, 3905 selection, selectionArgs, null, -1 /* directory ID */, 3906 true /* suppress profile check */); 3907 try { 3908 while(c.moveToNext()) { 3909 // Check profile permission for the raw contact that owns each data record. 3910 long rawContactId = c.getLong(DataRowHandler.DataUpdateQuery.RAW_CONTACT_ID); 3911 enforceProfilePermissionForRawContact(mDb, rawContactId, true); 3912 3913 count += updateData(mValues, c, callerIsSyncAdapter); 3914 } 3915 } finally { 3916 c.close(); 3917 } 3918 3919 return count; 3920 } 3921 3922 private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter) { 3923 if (values.size() == 0) { 3924 return 0; 3925 } 3926 3927 final String mimeType = c.getString(DataRowHandler.DataUpdateQuery.MIMETYPE); 3928 DataRowHandler rowHandler = getDataRowHandler(mimeType); 3929 boolean updated = 3930 rowHandler.update(mDb, mTransactionContext, values, c, callerIsSyncAdapter); 3931 if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) { 3932 scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS); 3933 } 3934 return updated ? 1 : 0; 3935 } 3936 3937 private int updateContactOptions(ContentValues values, String selection, 3938 String[] selectionArgs, boolean callerIsSyncAdapter) { 3939 int count = 0; 3940 Cursor cursor = mDb.query(Views.CONTACTS, 3941 new String[] { Contacts._ID, Contacts.IS_USER_PROFILE }, selection, 3942 selectionArgs, null, null, null); 3943 try { 3944 while (cursor.moveToNext()) { 3945 long contactId = cursor.getLong(0); 3946 3947 // Check for profile write permission before updating a user's profile contact. 3948 boolean isProfile = cursor.getInt(1) == 1; 3949 if (isProfile) { 3950 enforceProfilePermission(true); 3951 } 3952 3953 updateContactOptions(contactId, values, callerIsSyncAdapter); 3954 count++; 3955 } 3956 } finally { 3957 cursor.close(); 3958 } 3959 3960 return count; 3961 } 3962 3963 private int updateContactOptions(long contactId, ContentValues values, 3964 boolean callerIsSyncAdapter) { 3965 3966 // Check write permission if the contact is the user's profile. 3967 enforceProfilePermissionForContact(mDb, contactId, true); 3968 3969 mValues.clear(); 3970 ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE, 3971 values, Contacts.CUSTOM_RINGTONE); 3972 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL, 3973 values, Contacts.SEND_TO_VOICEMAIL); 3974 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED, 3975 values, Contacts.LAST_TIME_CONTACTED); 3976 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED, 3977 values, Contacts.TIMES_CONTACTED); 3978 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED, 3979 values, Contacts.STARRED); 3980 3981 // Nothing to update - just return 3982 if (mValues.size() == 0) { 3983 return 0; 3984 } 3985 3986 if (mValues.containsKey(RawContacts.STARRED)) { 3987 // Mark dirty when changing starred to trigger sync 3988 mValues.put(RawContacts.DIRTY, 1); 3989 } 3990 3991 mSelectionArgs1[0] = String.valueOf(contactId); 3992 mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=?" 3993 + " AND " + RawContacts.RAW_CONTACT_IS_READ_ONLY + "=0", mSelectionArgs1); 3994 3995 if (mValues.containsKey(RawContacts.STARRED) && !callerIsSyncAdapter) { 3996 Cursor cursor = mDb.query(Views.RAW_CONTACTS, 3997 new String[] { RawContacts._ID }, RawContacts.CONTACT_ID + "=?", 3998 mSelectionArgs1, null, null, null); 3999 try { 4000 while (cursor.moveToNext()) { 4001 long rawContactId = cursor.getLong(0); 4002 updateFavoritesMembership(rawContactId, 4003 mValues.getAsLong(RawContacts.STARRED) != 0); 4004 } 4005 } finally { 4006 cursor.close(); 4007 } 4008 } 4009 4010 // Copy changeable values to prevent automatically managed fields from 4011 // being explicitly updated by clients. 4012 mValues.clear(); 4013 ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE, 4014 values, Contacts.CUSTOM_RINGTONE); 4015 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL, 4016 values, Contacts.SEND_TO_VOICEMAIL); 4017 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED, 4018 values, Contacts.LAST_TIME_CONTACTED); 4019 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED, 4020 values, Contacts.TIMES_CONTACTED); 4021 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED, 4022 values, Contacts.STARRED); 4023 4024 int rslt = mDb.update(Tables.CONTACTS, mValues, Contacts._ID + "=?", mSelectionArgs1); 4025 4026 if (values.containsKey(Contacts.LAST_TIME_CONTACTED) && 4027 !values.containsKey(Contacts.TIMES_CONTACTED)) { 4028 mDb.execSQL(UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1); 4029 mDb.execSQL(UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1); 4030 } 4031 return rslt; 4032 } 4033 4034 private int updateAggregationException(SQLiteDatabase db, ContentValues values) { 4035 int exceptionType = values.getAsInteger(AggregationExceptions.TYPE); 4036 long rcId1 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID1); 4037 long rcId2 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID2); 4038 4039 long rawContactId1; 4040 long rawContactId2; 4041 if (rcId1 < rcId2) { 4042 rawContactId1 = rcId1; 4043 rawContactId2 = rcId2; 4044 } else { 4045 rawContactId2 = rcId1; 4046 rawContactId1 = rcId2; 4047 } 4048 4049 if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) { 4050 mSelectionArgs2[0] = String.valueOf(rawContactId1); 4051 mSelectionArgs2[1] = String.valueOf(rawContactId2); 4052 db.delete(Tables.AGGREGATION_EXCEPTIONS, 4053 AggregationExceptions.RAW_CONTACT_ID1 + "=? AND " 4054 + AggregationExceptions.RAW_CONTACT_ID2 + "=?", mSelectionArgs2); 4055 } else { 4056 ContentValues exceptionValues = new ContentValues(3); 4057 exceptionValues.put(AggregationExceptions.TYPE, exceptionType); 4058 exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1); 4059 exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2); 4060 db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID, 4061 exceptionValues); 4062 } 4063 4064 mContactAggregator.invalidateAggregationExceptionCache(); 4065 mContactAggregator.markForAggregation(rawContactId1, 4066 RawContacts.AGGREGATION_MODE_DEFAULT, true); 4067 mContactAggregator.markForAggregation(rawContactId2, 4068 RawContacts.AGGREGATION_MODE_DEFAULT, true); 4069 4070 mContactAggregator.aggregateContact(mTransactionContext, db, rawContactId1); 4071 mContactAggregator.aggregateContact(mTransactionContext, db, rawContactId2); 4072 4073 // The return value is fake - we just confirm that we made a change, not count actual 4074 // rows changed. 4075 return 1; 4076 } 4077 4078 public void onAccountsUpdated(Account[] accounts) { 4079 scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_ACCOUNTS); 4080 } 4081 4082 protected boolean updateAccountsInBackground(Account[] accounts) { 4083 // TODO : Check the unit test. 4084 boolean accountsChanged = false; 4085 mDb = mDbHelper.getWritableDatabase(); 4086 mDb.beginTransaction(); 4087 try { 4088 Set<AccountWithDataSet> existingAccountsWithDataSets = 4089 findValidAccountsWithDataSets(Tables.ACCOUNTS); 4090 4091 // Add a row to the ACCOUNTS table (with no data set) for each new account. 4092 for (Account account : accounts) { 4093 AccountWithDataSet accountWithDataSet = new AccountWithDataSet( 4094 account.name, account.type, null); 4095 if (!existingAccountsWithDataSets.contains(accountWithDataSet)) { 4096 accountsChanged = true; 4097 4098 // Add an account entry with an empty data set to match the account. 4099 mDb.execSQL("INSERT INTO " + Tables.ACCOUNTS + " (" + RawContacts.ACCOUNT_NAME 4100 + ", " + RawContacts.ACCOUNT_TYPE + ", " + RawContacts.DATA_SET 4101 + ") VALUES (?, ?, ?)", 4102 new String[] { 4103 accountWithDataSet.getAccountName(), 4104 accountWithDataSet.getAccountType(), 4105 accountWithDataSet.getDataSet() 4106 }); 4107 } 4108 } 4109 4110 // Check each of the existing sub-accounts against the account list. If the owning 4111 // account no longer exists, the sub-account and all its data should be deleted. 4112 List<AccountWithDataSet> accountsWithDataSetsToDelete = 4113 new ArrayList<AccountWithDataSet>(); 4114 List<Account> accountList = Arrays.asList(accounts); 4115 for (AccountWithDataSet accountWithDataSet : existingAccountsWithDataSets) { 4116 Account owningAccount = new Account( 4117 accountWithDataSet.getAccountName(), accountWithDataSet.getAccountType()); 4118 if (!accountList.contains(owningAccount)) { 4119 accountsWithDataSetsToDelete.add(accountWithDataSet); 4120 } 4121 } 4122 4123 if (!accountsWithDataSetsToDelete.isEmpty()) { 4124 accountsChanged = true; 4125 for (AccountWithDataSet accountWithDataSet : accountsWithDataSetsToDelete) { 4126 Log.d(TAG, "removing data for removed account " + accountWithDataSet); 4127 String[] accountParams = new String[] { 4128 accountWithDataSet.getAccountName(), 4129 accountWithDataSet.getAccountType() 4130 }; 4131 String[] accountWithDataSetParams = accountWithDataSet.getDataSet() == null 4132 ? accountParams 4133 : new String[] { 4134 accountWithDataSet.getAccountName(), 4135 accountWithDataSet.getAccountType(), 4136 accountWithDataSet.getDataSet() 4137 }; 4138 String groupsDataSetClause = " AND " + Groups.DATA_SET 4139 + (accountWithDataSet.getDataSet() == null ? " IS NULL" : " = ?"); 4140 String rawContactsDataSetClause = " AND " + RawContacts.DATA_SET 4141 + (accountWithDataSet.getDataSet() == null ? " IS NULL" : " = ?"); 4142 4143 mDb.execSQL( 4144 "DELETE FROM " + Tables.GROUPS + 4145 " WHERE " + Groups.ACCOUNT_NAME + " = ?" + 4146 " AND " + Groups.ACCOUNT_TYPE + " = ?" + 4147 groupsDataSetClause, accountWithDataSetParams); 4148 mDb.execSQL( 4149 "DELETE FROM " + Tables.PRESENCE + 4150 " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (" + 4151 "SELECT " + RawContacts._ID + 4152 " FROM " + Tables.RAW_CONTACTS + 4153 " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" + 4154 " AND " + RawContacts.ACCOUNT_TYPE + " = ?" + 4155 rawContactsDataSetClause + ")", accountWithDataSetParams); 4156 mDb.execSQL( 4157 "DELETE FROM " + Tables.RAW_CONTACTS + 4158 " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" + 4159 " AND " + RawContacts.ACCOUNT_TYPE + " = ?" + 4160 rawContactsDataSetClause, accountWithDataSetParams); 4161 mDb.execSQL( 4162 "DELETE FROM " + Tables.SETTINGS + 4163 " WHERE " + Settings.ACCOUNT_NAME + " = ?" + 4164 " AND " + Settings.ACCOUNT_TYPE + " = ?", accountParams); 4165 mDb.execSQL( 4166 "DELETE FROM " + Tables.ACCOUNTS + 4167 " WHERE " + RawContacts.ACCOUNT_NAME + "=?" + 4168 " AND " + RawContacts.ACCOUNT_TYPE + "=?" + 4169 rawContactsDataSetClause, accountWithDataSetParams); 4170 mDb.execSQL( 4171 "DELETE FROM " + Tables.DIRECTORIES + 4172 " WHERE " + Directory.ACCOUNT_NAME + "=?" + 4173 " AND " + Directory.ACCOUNT_TYPE + "=?", accountParams); 4174 resetDirectoryCache(); 4175 } 4176 4177 // Find all aggregated contacts that used to contain the raw contacts 4178 // we have just deleted and see if they are still referencing the deleted 4179 // names or photos. If so, fix up those contacts. 4180 HashSet<Long> orphanContactIds = Sets.newHashSet(); 4181 Cursor cursor = mDb.rawQuery("SELECT " + Contacts._ID + 4182 " FROM " + Tables.CONTACTS + 4183 " WHERE (" + Contacts.NAME_RAW_CONTACT_ID + " NOT NULL AND " + 4184 Contacts.NAME_RAW_CONTACT_ID + " NOT IN " + 4185 "(SELECT " + RawContacts._ID + 4186 " FROM " + Tables.RAW_CONTACTS + "))" + 4187 " OR (" + Contacts.PHOTO_ID + " NOT NULL AND " + 4188 Contacts.PHOTO_ID + " NOT IN " + 4189 "(SELECT " + Data._ID + 4190 " FROM " + Tables.DATA + "))", null); 4191 try { 4192 while (cursor.moveToNext()) { 4193 orphanContactIds.add(cursor.getLong(0)); 4194 } 4195 } finally { 4196 cursor.close(); 4197 } 4198 4199 for (Long contactId : orphanContactIds) { 4200 mContactAggregator.updateAggregateData(mTransactionContext, contactId); 4201 } 4202 mDbHelper.updateAllVisible(); 4203 updateSearchIndexInTransaction(); 4204 } 4205 4206 // Now that we've done the account-based additions and subtractions from the Accounts 4207 // table, check for raw contacts that have been added with a data set and add Accounts 4208 // entries for those if necessary. 4209 existingAccountsWithDataSets = findValidAccountsWithDataSets(Tables.ACCOUNTS); 4210 Set<AccountWithDataSet> rawContactAccountsWithDataSets = 4211 findValidAccountsWithDataSets(Tables.RAW_CONTACTS); 4212 rawContactAccountsWithDataSets.removeAll(existingAccountsWithDataSets); 4213 4214 // Any remaining raw contact sub-accounts need to be added to the Accounts table. 4215 for (AccountWithDataSet accountWithDataSet : rawContactAccountsWithDataSets) { 4216 accountsChanged = true; 4217 4218 // Add an account entry to match the raw contact. 4219 mDb.execSQL("INSERT INTO " + Tables.ACCOUNTS + " (" + RawContacts.ACCOUNT_NAME 4220 + ", " + RawContacts.ACCOUNT_TYPE + ", " + RawContacts.DATA_SET 4221 + ") VALUES (?, ?, ?)", 4222 new String[] { 4223 accountWithDataSet.getAccountName(), 4224 accountWithDataSet.getAccountType(), 4225 accountWithDataSet.getDataSet() 4226 }); 4227 } 4228 4229 if (accountsChanged) { 4230 // TODO: Should sync state take data set into consideration? 4231 mDbHelper.getSyncState().onAccountsChanged(mDb, accounts); 4232 } 4233 mDb.setTransactionSuccessful(); 4234 } finally { 4235 mDb.endTransaction(); 4236 } 4237 mAccountWritability.clear(); 4238 4239 if (accountsChanged) { 4240 updateContactsAccountCount(accounts); 4241 updateProviderStatus(); 4242 } 4243 4244 return accountsChanged; 4245 } 4246 4247 private void updateContactsAccountCount(Account[] accounts) { 4248 int count = 0; 4249 for (Account account : accounts) { 4250 if (isContactsAccount(account)) { 4251 count++; 4252 } 4253 } 4254 mContactsAccountCount = count; 4255 } 4256 4257 protected boolean isContactsAccount(Account account) { 4258 final IContentService cs = ContentResolver.getContentService(); 4259 try { 4260 return cs.getIsSyncable(account, ContactsContract.AUTHORITY) > 0; 4261 } catch (RemoteException e) { 4262 Log.e(TAG, "Cannot obtain sync flag for account: " + account, e); 4263 return false; 4264 } 4265 } 4266 4267 public void onPackageChanged(String packageName) { 4268 scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_DIRECTORIES, packageName); 4269 } 4270 4271 /** 4272 * Finds all distinct account types and data sets present in the specified table. 4273 */ 4274 private Set<AccountWithDataSet> findValidAccountsWithDataSets(String table) { 4275 Set<AccountWithDataSet> accountsWithDataSets = new HashSet<AccountWithDataSet>(); 4276 Cursor c = mDb.rawQuery( 4277 "SELECT DISTINCT " + RawContacts.ACCOUNT_NAME + "," + RawContacts.ACCOUNT_TYPE + 4278 "," + RawContacts.DATA_SET + 4279 " FROM " + table, null); 4280 try { 4281 while (c.moveToNext()) { 4282 if (!c.isNull(0) || !c.isNull(1)) { 4283 accountsWithDataSets.add( 4284 new AccountWithDataSet(c.getString(0), c.getString(1), c.getString(2))); 4285 } 4286 } 4287 } finally { 4288 c.close(); 4289 } 4290 return accountsWithDataSets; 4291 } 4292 4293 @Override 4294 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 4295 String sortOrder) { 4296 4297 waitForAccess(mReadAccessLatch); 4298 4299 String directory = getQueryParameter(uri, ContactsContract.DIRECTORY_PARAM_KEY); 4300 if (directory == null) { 4301 return wrapCursor(uri, 4302 queryLocal(uri, projection, selection, selectionArgs, sortOrder, -1, false)); 4303 } else if (directory.equals("0")) { 4304 return wrapCursor(uri, 4305 queryLocal(uri, projection, selection, selectionArgs, sortOrder, 4306 Directory.DEFAULT, false)); 4307 } else if (directory.equals("1")) { 4308 return wrapCursor(uri, 4309 queryLocal(uri, projection, selection, selectionArgs, sortOrder, 4310 Directory.LOCAL_INVISIBLE, false)); 4311 } 4312 4313 DirectoryInfo directoryInfo = getDirectoryAuthority(directory); 4314 if (directoryInfo == null) { 4315 Log.e(TAG, "Invalid directory ID: " + uri); 4316 return null; 4317 } 4318 4319 Builder builder = new Uri.Builder(); 4320 builder.scheme(ContentResolver.SCHEME_CONTENT); 4321 builder.authority(directoryInfo.authority); 4322 builder.encodedPath(uri.getEncodedPath()); 4323 if (directoryInfo.accountName != null) { 4324 builder.appendQueryParameter(RawContacts.ACCOUNT_NAME, directoryInfo.accountName); 4325 } 4326 if (directoryInfo.accountType != null) { 4327 builder.appendQueryParameter(RawContacts.ACCOUNT_TYPE, directoryInfo.accountType); 4328 } 4329 4330 String limit = getLimit(uri); 4331 if (limit != null) { 4332 builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, limit); 4333 } 4334 4335 Uri directoryUri = builder.build(); 4336 4337 if (projection == null) { 4338 projection = getDefaultProjection(uri); 4339 } 4340 4341 Cursor cursor = getContext().getContentResolver().query(directoryUri, projection, selection, 4342 selectionArgs, sortOrder); 4343 4344 if (cursor == null) { 4345 return null; 4346 } 4347 4348 CrossProcessCursor crossProcessCursor = getCrossProcessCursor(cursor); 4349 if (crossProcessCursor != null) { 4350 return wrapCursor(uri, cursor); 4351 } else { 4352 return matrixCursorFromCursor(wrapCursor(uri, cursor)); 4353 } 4354 } 4355 4356 private Cursor wrapCursor(Uri uri, Cursor cursor) { 4357 4358 // If the cursor doesn't contain a snippet column, don't bother wrapping it. 4359 if (cursor.getColumnIndex(SearchSnippetColumns.SNIPPET) < 0) { 4360 if (VERBOSE_LOGGING) { 4361 return new InstrumentedCursorWrapper(cursor, uri, TAG); 4362 } else { 4363 return cursor; 4364 } 4365 } 4366 4367 // Parse out snippet arguments for use when snippets are retrieved from the cursor. 4368 String[] args = null; 4369 String snippetArgs = 4370 getQueryParameter(uri, SearchSnippetColumns.SNIPPET_ARGS_PARAM_KEY); 4371 if (snippetArgs != null) { 4372 args = snippetArgs.split(","); 4373 } 4374 4375 String query = uri.getLastPathSegment(); 4376 String startMatch = args != null && args.length > 0 ? args[0] 4377 : DEFAULT_SNIPPET_ARG_START_MATCH; 4378 String endMatch = args != null && args.length > 1 ? args[1] 4379 : DEFAULT_SNIPPET_ARG_END_MATCH; 4380 String ellipsis = args != null && args.length > 2 ? args[2] 4381 : DEFAULT_SNIPPET_ARG_ELLIPSIS; 4382 int maxTokens = args != null && args.length > 3 ? Integer.parseInt(args[3]) 4383 : DEFAULT_SNIPPET_ARG_MAX_TOKENS; 4384 4385 if (VERBOSE_LOGGING) { 4386 return new InstrumentedCursorWrapper(new SnippetizingCursorWrapper( 4387 cursor, query, startMatch, endMatch, ellipsis, maxTokens), uri, TAG); 4388 } else { 4389 return new SnippetizingCursorWrapper(cursor, query, startMatch, endMatch, ellipsis, 4390 maxTokens); 4391 } 4392 } 4393 4394 private CrossProcessCursor getCrossProcessCursor(Cursor cursor) { 4395 Cursor c = cursor; 4396 if (c instanceof CrossProcessCursor) { 4397 return (CrossProcessCursor) c; 4398 } else if (c instanceof CursorWindow) { 4399 return getCrossProcessCursor(((CursorWrapper) c).getWrappedCursor()); 4400 } else { 4401 return null; 4402 } 4403 } 4404 4405 public MatrixCursor matrixCursorFromCursor(Cursor cursor) { 4406 MatrixCursor newCursor = new MatrixCursor(cursor.getColumnNames()); 4407 int numColumns = cursor.getColumnCount(); 4408 String data[] = new String[numColumns]; 4409 cursor.moveToPosition(-1); 4410 while (cursor.moveToNext()) { 4411 for (int i = 0; i < numColumns; i++) { 4412 data[i] = cursor.getString(i); 4413 } 4414 newCursor.addRow(data); 4415 } 4416 return newCursor; 4417 } 4418 4419 private static final class DirectoryQuery { 4420 public static final String[] COLUMNS = new String[] { 4421 Directory._ID, 4422 Directory.DIRECTORY_AUTHORITY, 4423 Directory.ACCOUNT_NAME, 4424 Directory.ACCOUNT_TYPE 4425 }; 4426 4427 public static final int DIRECTORY_ID = 0; 4428 public static final int AUTHORITY = 1; 4429 public static final int ACCOUNT_NAME = 2; 4430 public static final int ACCOUNT_TYPE = 3; 4431 } 4432 4433 /** 4434 * Reads and caches directory information for the database. 4435 */ 4436 private DirectoryInfo getDirectoryAuthority(String directoryId) { 4437 synchronized (mDirectoryCache) { 4438 if (!mDirectoryCacheValid) { 4439 mDirectoryCache.clear(); 4440 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 4441 Cursor cursor = db.query(Tables.DIRECTORIES, 4442 DirectoryQuery.COLUMNS, 4443 null, null, null, null, null); 4444 try { 4445 while (cursor.moveToNext()) { 4446 DirectoryInfo info = new DirectoryInfo(); 4447 String id = cursor.getString(DirectoryQuery.DIRECTORY_ID); 4448 info.authority = cursor.getString(DirectoryQuery.AUTHORITY); 4449 info.accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME); 4450 info.accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE); 4451 mDirectoryCache.put(id, info); 4452 } 4453 } finally { 4454 cursor.close(); 4455 } 4456 mDirectoryCacheValid = true; 4457 } 4458 4459 return mDirectoryCache.get(directoryId); 4460 } 4461 } 4462 4463 public void resetDirectoryCache() { 4464 synchronized(mDirectoryCache) { 4465 mDirectoryCacheValid = false; 4466 } 4467 } 4468 4469 private Cursor queryLocal(Uri uri, String[] projection, String selection, 4470 String[] selectionArgs, String sortOrder, long directoryId, 4471 final boolean suppressProfileCheck) { 4472 if (VERBOSE_LOGGING) { 4473 Log.v(TAG, "query: " + uri); 4474 } 4475 4476 final SQLiteDatabase db = mDbHelper.getReadableDatabase(); 4477 4478 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 4479 String groupBy = null; 4480 String limit = getLimit(uri); 4481 4482 // Column name for appendProfileRestriction(). We append the profile check to the original 4483 // selection if it's not null. 4484 String profileRestrictionColumnName = null; 4485 4486 final int match = sUriMatcher.match(uri); 4487 switch (match) { 4488 case SYNCSTATE: 4489 return mDbHelper.getSyncState().query(db, projection, selection, selectionArgs, 4490 sortOrder); 4491 4492 case CONTACTS: { 4493 setTablesAndProjectionMapForContacts(qb, uri, projection); 4494 appendLocalDirectorySelectionIfNeeded(qb, directoryId); 4495 profileRestrictionColumnName = Contacts.IS_USER_PROFILE; 4496 sortOrder = prependProfileSortIfNeeded(uri, sortOrder, suppressProfileCheck); 4497 break; 4498 } 4499 4500 case CONTACTS_ID: { 4501 long contactId = ContentUris.parseId(uri); 4502 enforceProfilePermissionForContact(db, contactId, false); 4503 setTablesAndProjectionMapForContacts(qb, uri, projection); 4504 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 4505 qb.appendWhere(Contacts._ID + "=?"); 4506 break; 4507 } 4508 4509 case CONTACTS_LOOKUP: 4510 case CONTACTS_LOOKUP_ID: { 4511 List<String> pathSegments = uri.getPathSegments(); 4512 int segmentCount = pathSegments.size(); 4513 if (segmentCount < 3) { 4514 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 4515 "Missing a lookup key", uri)); 4516 } 4517 4518 String lookupKey = pathSegments.get(2); 4519 if (segmentCount == 4) { 4520 long contactId = Long.parseLong(pathSegments.get(3)); 4521 enforceProfilePermissionForContact(db, contactId, false); 4522 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 4523 setTablesAndProjectionMapForContacts(lookupQb, uri, projection); 4524 4525 Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri, 4526 projection, selection, selectionArgs, sortOrder, groupBy, limit, 4527 Contacts._ID, contactId, Contacts.LOOKUP_KEY, lookupKey); 4528 if (c != null) { 4529 return c; 4530 } 4531 } 4532 4533 setTablesAndProjectionMapForContacts(qb, uri, projection); 4534 selectionArgs = insertSelectionArg(selectionArgs, 4535 String.valueOf(lookupContactIdByLookupKey(db, lookupKey))); 4536 qb.appendWhere(Contacts._ID + "=?"); 4537 break; 4538 } 4539 4540 case CONTACTS_LOOKUP_DATA: 4541 case CONTACTS_LOOKUP_ID_DATA: { 4542 List<String> pathSegments = uri.getPathSegments(); 4543 int segmentCount = pathSegments.size(); 4544 if (segmentCount < 4) { 4545 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 4546 "Missing a lookup key", uri)); 4547 } 4548 String lookupKey = pathSegments.get(2); 4549 if (segmentCount == 5) { 4550 long contactId = Long.parseLong(pathSegments.get(3)); 4551 enforceProfilePermissionForContact(db, contactId, false); 4552 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 4553 setTablesAndProjectionMapForData(lookupQb, uri, projection, false); 4554 lookupQb.appendWhere(" AND "); 4555 Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri, 4556 projection, selection, selectionArgs, sortOrder, groupBy, limit, 4557 Data.CONTACT_ID, contactId, Data.LOOKUP_KEY, lookupKey); 4558 if (c != null) { 4559 return c; 4560 } 4561 4562 // TODO see if the contact exists but has no data rows (rare) 4563 } 4564 4565 setTablesAndProjectionMapForData(qb, uri, projection, false); 4566 long contactId = lookupContactIdByLookupKey(db, lookupKey); 4567 enforceProfilePermissionForContact(db, contactId, false); 4568 selectionArgs = insertSelectionArg(selectionArgs, 4569 String.valueOf(contactId)); 4570 qb.appendWhere(" AND " + Data.CONTACT_ID + "=?"); 4571 break; 4572 } 4573 4574 case CONTACTS_ID_STREAM_ITEMS: { 4575 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 4576 enforceProfilePermissionForContact(db, contactId, false); 4577 setTablesAndProjectionMapForStreamItems(qb); 4578 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 4579 qb.appendWhere(RawContactsColumns.CONCRETE_CONTACT_ID + "=?"); 4580 break; 4581 } 4582 4583 case CONTACTS_LOOKUP_STREAM_ITEMS: 4584 case CONTACTS_LOOKUP_ID_STREAM_ITEMS: { 4585 List<String> pathSegments = uri.getPathSegments(); 4586 int segmentCount = pathSegments.size(); 4587 if (segmentCount < 4) { 4588 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 4589 "Missing a lookup key", uri)); 4590 } 4591 String lookupKey = pathSegments.get(2); 4592 if (segmentCount == 5) { 4593 long contactId = Long.parseLong(pathSegments.get(3)); 4594 enforceProfilePermissionForContact(db, contactId, false); 4595 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 4596 setTablesAndProjectionMapForStreamItems(lookupQb); 4597 Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri, 4598 projection, selection, selectionArgs, sortOrder, groupBy, limit, 4599 RawContacts.CONTACT_ID, contactId, Contacts.LOOKUP_KEY, lookupKey); 4600 if (c != null) { 4601 return c; 4602 } 4603 } 4604 4605 setTablesAndProjectionMapForStreamItems(qb); 4606 long contactId = lookupContactIdByLookupKey(db, lookupKey); 4607 enforceProfilePermissionForContact(db, contactId, false); 4608 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 4609 qb.appendWhere(RawContacts.CONTACT_ID + "=?"); 4610 break; 4611 } 4612 4613 case CONTACTS_AS_VCARD: { 4614 final String lookupKey = Uri.encode(uri.getPathSegments().get(2)); 4615 long contactId = lookupContactIdByLookupKey(db, lookupKey); 4616 enforceProfilePermissionForContact(db, contactId, false); 4617 qb.setTables(Views.CONTACTS); 4618 qb.setProjectionMap(sContactsVCardProjectionMap); 4619 selectionArgs = insertSelectionArg(selectionArgs, 4620 String.valueOf(contactId)); 4621 qb.appendWhere(Contacts._ID + "=?"); 4622 break; 4623 } 4624 4625 case CONTACTS_AS_MULTI_VCARD: { 4626 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss"); 4627 String currentDateString = dateFormat.format(new Date()).toString(); 4628 return db.rawQuery( 4629 "SELECT" + 4630 " 'vcards_' || ? || '.vcf' AS " + OpenableColumns.DISPLAY_NAME + "," + 4631 " NULL AS " + OpenableColumns.SIZE, 4632 new String[] { currentDateString }); 4633 } 4634 4635 case CONTACTS_FILTER: { 4636 String filterParam = ""; 4637 if (uri.getPathSegments().size() > 2) { 4638 filterParam = uri.getLastPathSegment(); 4639 } 4640 setTablesAndProjectionMapForContactsWithSnippet( 4641 qb, uri, projection, filterParam, directoryId); 4642 profileRestrictionColumnName = Contacts.IS_USER_PROFILE; 4643 sortOrder = prependProfileSortIfNeeded(uri, sortOrder, suppressProfileCheck); 4644 break; 4645 } 4646 4647 case CONTACTS_STREQUENT_FILTER: 4648 case CONTACTS_STREQUENT: { 4649 // Basically the resultant SQL should look like this: 4650 // (SQL for listing starred items) 4651 // UNION ALL 4652 // (SQL for listing frequently contacted items) 4653 // ORDER BY ... 4654 4655 final boolean phoneOnly = readBooleanQueryParameter( 4656 uri, ContactsContract.STREQUENT_PHONE_ONLY, false); 4657 if (match == CONTACTS_STREQUENT_FILTER && uri.getPathSegments().size() > 3) { 4658 String filterParam = uri.getLastPathSegment(); 4659 StringBuilder sb = new StringBuilder(); 4660 sb.append(Contacts._ID + " IN "); 4661 appendContactFilterAsNestedQuery(sb, filterParam); 4662 selection = DbQueryUtils.concatenateClauses(selection, sb.toString()); 4663 } 4664 4665 String[] subProjection = null; 4666 if (projection != null) { 4667 subProjection = appendProjectionArg(projection, TIMES_USED_SORT_COLUMN); 4668 } 4669 4670 // Build the first query for starred 4671 setTablesAndProjectionMapForContacts(qb, uri, projection, false); 4672 qb.setProjectionMap(phoneOnly ? 4673 sStrequentPhoneOnlyStarredProjectionMap 4674 : sStrequentStarredProjectionMap); 4675 qb.appendWhere(DbQueryUtils.concatenateClauses( 4676 selection, Contacts.IS_USER_PROFILE + "=0")); 4677 if (phoneOnly) { 4678 qb.appendWhere(" AND " + Contacts.HAS_PHONE_NUMBER + "=1"); 4679 } 4680 qb.setStrict(true); 4681 final String starredQuery = qb.buildQuery(subProjection, 4682 Contacts.STARRED + "=1", Contacts._ID, null, null, null); 4683 4684 // Reset the builder. 4685 qb = new SQLiteQueryBuilder(); 4686 qb.setStrict(true); 4687 4688 // Build the second query for frequent part. 4689 final String frequentQuery; 4690 if (phoneOnly) { 4691 final StringBuilder tableBuilder = new StringBuilder(); 4692 // In phone only mode, we need to look at view_data instead of 4693 // contacts/raw_contacts to obtain actual phone numbers. One problem is that 4694 // view_data is much larger than view_contacts, so our query might become much 4695 // slower. 4696 // 4697 // To avoid the possible slow down, we start from data usage table and join 4698 // view_data to the table, assuming data usage table is quite smaller than 4699 // data rows (almost always it should be), and we don't want any phone 4700 // numbers not used by the user. This way sqlite is able to drop a number of 4701 // rows in view_data in the early stage of data lookup. 4702 tableBuilder.append(Tables.DATA_USAGE_STAT 4703 + " INNER JOIN " + Views.DATA + " " + Tables.DATA 4704 + " ON (" + DataUsageStatColumns.CONCRETE_DATA_ID + "=" 4705 + DataColumns.CONCRETE_ID + " AND " 4706 + DataUsageStatColumns.CONCRETE_USAGE_TYPE + "=" 4707 + DataUsageStatColumns.USAGE_TYPE_INT_CALL + ")"); 4708 appendContactPresenceJoin(tableBuilder, projection, RawContacts.CONTACT_ID); 4709 appendContactStatusUpdateJoin(tableBuilder, projection, 4710 ContactsColumns.LAST_STATUS_UPDATE_ID); 4711 4712 qb.setTables(tableBuilder.toString()); 4713 qb.setProjectionMap(sStrequentPhoneOnlyFrequentProjectionMap); 4714 qb.appendWhere(DbQueryUtils.concatenateClauses( 4715 selection, 4716 Contacts.STARRED + "=0 OR " + Contacts.STARRED + " IS NULL", 4717 MimetypesColumns.MIMETYPE + " IN (" 4718 + "'" + Phone.CONTENT_ITEM_TYPE + "', " 4719 + "'" + SipAddress.CONTENT_ITEM_TYPE + "')")); 4720 frequentQuery = qb.buildQuery(subProjection, null, null, null, null, null); 4721 } else { 4722 setTablesAndProjectionMapForContacts(qb, uri, projection, true); 4723 qb.setProjectionMap(sStrequentFrequentProjectionMap); 4724 qb.appendWhere(DbQueryUtils.concatenateClauses( 4725 selection, 4726 "(" + Contacts.STARRED + " =0 OR " + Contacts.STARRED + " IS NULL)", 4727 Contacts.IS_USER_PROFILE + "=0")); 4728 frequentQuery = qb.buildQuery(subProjection, 4729 null, Contacts._ID, null, null, null); 4730 } 4731 4732 // Put them together 4733 final String unionQuery = 4734 qb.buildUnionQuery(new String[] {starredQuery, frequentQuery}, 4735 STREQUENT_ORDER_BY, STREQUENT_LIMIT); 4736 4737 // Here, we need to use selection / selectionArgs (supplied from users) "twice", 4738 // as we want them both for starred items and for frequently contacted items. 4739 // 4740 // e.g. if the user specify selection = "starred =?" and selectionArgs = "0", 4741 // the resultant SQL should be like: 4742 // SELECT ... WHERE starred =? AND ... 4743 // UNION ALL 4744 // SELECT ... WHERE starred =? AND ... 4745 String[] doubledSelectionArgs = null; 4746 if (selectionArgs != null) { 4747 final int length = selectionArgs.length; 4748 doubledSelectionArgs = new String[length * 2]; 4749 System.arraycopy(selectionArgs, 0, doubledSelectionArgs, 0, length); 4750 System.arraycopy(selectionArgs, 0, doubledSelectionArgs, length, length); 4751 } 4752 4753 Cursor cursor = db.rawQuery(unionQuery, doubledSelectionArgs); 4754 if (cursor != null) { 4755 cursor.setNotificationUri(getContext().getContentResolver(), 4756 ContactsContract.AUTHORITY_URI); 4757 } 4758 return cursor; 4759 } 4760 4761 case CONTACTS_FREQUENT: { 4762 setTablesAndProjectionMapForContacts(qb, uri, projection, true); 4763 qb.setProjectionMap(sStrequentFrequentProjectionMap); 4764 qb.appendWhere(Contacts.IS_USER_PROFILE + "=0"); 4765 groupBy = Contacts._ID; 4766 if (!TextUtils.isEmpty(sortOrder)) { 4767 sortOrder = FREQUENT_ORDER_BY + ", " + sortOrder; 4768 } else { 4769 sortOrder = FREQUENT_ORDER_BY; 4770 } 4771 break; 4772 } 4773 4774 case CONTACTS_GROUP: { 4775 setTablesAndProjectionMapForContacts(qb, uri, projection); 4776 if (uri.getPathSegments().size() > 2) { 4777 qb.appendWhere(CONTACTS_IN_GROUP_SELECT); 4778 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 4779 } 4780 break; 4781 } 4782 4783 case PROFILE: { 4784 enforceProfilePermission(false); 4785 setTablesAndProjectionMapForContacts(qb, uri, projection); 4786 qb.appendWhere(Contacts.IS_USER_PROFILE + "=1"); 4787 break; 4788 } 4789 4790 case PROFILE_ENTITIES: { 4791 enforceProfilePermission(false); 4792 setTablesAndProjectionMapForEntities(qb, uri, projection); 4793 qb.appendWhere(" AND " + Contacts.IS_USER_PROFILE + "=1"); 4794 break; 4795 } 4796 4797 case PROFILE_DATA: { 4798 enforceProfilePermission(false); 4799 setTablesAndProjectionMapForData(qb, uri, projection, false); 4800 qb.appendWhere(" AND " + RawContacts.RAW_CONTACT_IS_USER_PROFILE + "=1"); 4801 break; 4802 } 4803 4804 case PROFILE_DATA_ID: { 4805 enforceProfilePermission(false); 4806 setTablesAndProjectionMapForData(qb, uri, projection, false); 4807 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 4808 qb.appendWhere(" AND " + Data._ID + "=? AND " 4809 + RawContacts.RAW_CONTACT_IS_USER_PROFILE + "=1"); 4810 break; 4811 } 4812 4813 case PROFILE_AS_VCARD: { 4814 enforceProfilePermission(false); 4815 qb.setTables(Views.CONTACTS); 4816 qb.setProjectionMap(sContactsVCardProjectionMap); 4817 qb.appendWhere(Contacts.IS_USER_PROFILE + "=1"); 4818 break; 4819 } 4820 4821 case CONTACTS_ID_DATA: { 4822 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 4823 setTablesAndProjectionMapForData(qb, uri, projection, false); 4824 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 4825 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?"); 4826 break; 4827 } 4828 4829 case CONTACTS_ID_PHOTO: { 4830 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 4831 enforceProfilePermissionForContact(db, contactId, false); 4832 setTablesAndProjectionMapForData(qb, uri, projection, false); 4833 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 4834 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?"); 4835 qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID); 4836 break; 4837 } 4838 4839 case CONTACTS_ID_ENTITIES: { 4840 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 4841 setTablesAndProjectionMapForEntities(qb, uri, projection); 4842 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 4843 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?"); 4844 break; 4845 } 4846 4847 case CONTACTS_LOOKUP_ENTITIES: 4848 case CONTACTS_LOOKUP_ID_ENTITIES: { 4849 List<String> pathSegments = uri.getPathSegments(); 4850 int segmentCount = pathSegments.size(); 4851 if (segmentCount < 4) { 4852 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 4853 "Missing a lookup key", uri)); 4854 } 4855 String lookupKey = pathSegments.get(2); 4856 if (segmentCount == 5) { 4857 long contactId = Long.parseLong(pathSegments.get(3)); 4858 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 4859 setTablesAndProjectionMapForEntities(lookupQb, uri, projection); 4860 lookupQb.appendWhere(" AND "); 4861 4862 Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri, 4863 projection, selection, selectionArgs, sortOrder, groupBy, limit, 4864 Contacts.Entity.CONTACT_ID, contactId, 4865 Contacts.Entity.LOOKUP_KEY, lookupKey); 4866 if (c != null) { 4867 return c; 4868 } 4869 } 4870 4871 setTablesAndProjectionMapForEntities(qb, uri, projection); 4872 selectionArgs = insertSelectionArg(selectionArgs, 4873 String.valueOf(lookupContactIdByLookupKey(db, lookupKey))); 4874 qb.appendWhere(" AND " + Contacts.Entity.CONTACT_ID + "=?"); 4875 break; 4876 } 4877 4878 case STREAM_ITEMS: { 4879 setTablesAndProjectionMapForStreamItems(qb); 4880 break; 4881 } 4882 4883 case STREAM_ITEMS_ID: { 4884 setTablesAndProjectionMapForStreamItems(qb); 4885 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 4886 qb.appendWhere(StreamItemsColumns.CONCRETE_ID + "=?"); 4887 break; 4888 } 4889 4890 case STREAM_ITEMS_LIMIT: { 4891 MatrixCursor cursor = new MatrixCursor(new String[]{StreamItems.MAX_ITEMS}, 1); 4892 cursor.addRow(new Object[]{MAX_STREAM_ITEMS_PER_RAW_CONTACT}); 4893 return cursor; 4894 } 4895 4896 case STREAM_ITEMS_PHOTOS: { 4897 setTablesAndProjectionMapForStreamItemPhotos(qb); 4898 break; 4899 } 4900 4901 case STREAM_ITEMS_ID_PHOTOS: { 4902 setTablesAndProjectionMapForStreamItemPhotos(qb); 4903 String streamItemId = uri.getPathSegments().get(1); 4904 selectionArgs = insertSelectionArg(selectionArgs, streamItemId); 4905 qb.appendWhere(StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID + "=?"); 4906 break; 4907 } 4908 4909 case STREAM_ITEMS_ID_PHOTOS_ID: { 4910 setTablesAndProjectionMapForStreamItemPhotos(qb); 4911 String streamItemId = uri.getPathSegments().get(1); 4912 String streamItemPhotoId = uri.getPathSegments().get(3); 4913 selectionArgs = insertSelectionArg(selectionArgs, streamItemPhotoId); 4914 selectionArgs = insertSelectionArg(selectionArgs, streamItemId); 4915 qb.appendWhere(StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID + "=? AND " + 4916 StreamItemPhotosColumns.CONCRETE_ID + "=?"); 4917 break; 4918 } 4919 4920 case PHOTO_DIMENSIONS: { 4921 MatrixCursor cursor = new MatrixCursor( 4922 new String[]{DisplayPhoto.DISPLAY_MAX_DIM, DisplayPhoto.THUMBNAIL_MAX_DIM}, 4923 1); 4924 cursor.addRow(new Object[]{mMaxDisplayPhotoDim, mMaxThumbnailPhotoDim}); 4925 return cursor; 4926 } 4927 4928 case PHONES: { 4929 setTablesAndProjectionMapForData(qb, uri, projection, false); 4930 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); 4931 break; 4932 } 4933 4934 case PHONES_ID: { 4935 setTablesAndProjectionMapForData(qb, uri, projection, false); 4936 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 4937 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); 4938 qb.appendWhere(" AND " + Data._ID + "=?"); 4939 break; 4940 } 4941 4942 case PHONES_FILTER: { 4943 String typeParam = uri.getQueryParameter(DataUsageFeedback.USAGE_TYPE); 4944 Integer typeInt = sDataUsageTypeMap.get(typeParam); 4945 if (typeInt == null) { 4946 typeInt = DataUsageStatColumns.USAGE_TYPE_INT_CALL; 4947 } 4948 setTablesAndProjectionMapForData(qb, uri, projection, true, typeInt); 4949 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); 4950 if (uri.getPathSegments().size() > 2) { 4951 String filterParam = uri.getLastPathSegment(); 4952 StringBuilder sb = new StringBuilder(); 4953 sb.append(" AND ("); 4954 4955 boolean hasCondition = false; 4956 boolean orNeeded = false; 4957 String normalizedName = NameNormalizer.normalize(filterParam); 4958 if (normalizedName.length() > 0) { 4959 sb.append(Data.RAW_CONTACT_ID + " IN " + 4960 "(SELECT " + RawContactsColumns.CONCRETE_ID + 4961 " FROM " + Tables.SEARCH_INDEX + 4962 " JOIN " + Tables.RAW_CONTACTS + 4963 " ON (" + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID 4964 + "=" + RawContactsColumns.CONCRETE_CONTACT_ID + ")" + 4965 " WHERE " + SearchIndexColumns.NAME + " MATCH "); 4966 DatabaseUtils.appendEscapedSQLString(sb, sanitizeMatch(filterParam) + "*"); 4967 sb.append(")"); 4968 orNeeded = true; 4969 hasCondition = true; 4970 } 4971 4972 String number = PhoneNumberUtils.normalizeNumber(filterParam); 4973 if (!TextUtils.isEmpty(number)) { 4974 if (orNeeded) { 4975 sb.append(" OR "); 4976 } 4977 sb.append(Data._ID + 4978 " IN (SELECT DISTINCT " + PhoneLookupColumns.DATA_ID 4979 + " FROM " + Tables.PHONE_LOOKUP 4980 + " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '"); 4981 sb.append(number); 4982 sb.append("%')"); 4983 hasCondition = true; 4984 } 4985 4986 if (!hasCondition) { 4987 // If it is neither a phone number nor a name, the query should return 4988 // an empty cursor. Let's ensure that. 4989 sb.append("0"); 4990 } 4991 sb.append(")"); 4992 qb.appendWhere(sb); 4993 } 4994 groupBy = PhoneColumns.NORMALIZED_NUMBER + "," + RawContacts.CONTACT_ID; 4995 if (sortOrder == null) { 4996 final String accountPromotionSortOrder = getAccountPromotionSortOrder(uri); 4997 if (!TextUtils.isEmpty(accountPromotionSortOrder)) { 4998 sortOrder = accountPromotionSortOrder + ", " + PHONE_FILTER_SORT_ORDER; 4999 } else { 5000 sortOrder = PHONE_FILTER_SORT_ORDER; 5001 } 5002 } 5003 break; 5004 } 5005 5006 case EMAILS: { 5007 setTablesAndProjectionMapForData(qb, uri, projection, false); 5008 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"); 5009 break; 5010 } 5011 5012 case EMAILS_ID: { 5013 setTablesAndProjectionMapForData(qb, uri, projection, false); 5014 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 5015 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'" 5016 + " AND " + Data._ID + "=?"); 5017 break; 5018 } 5019 5020 case EMAILS_LOOKUP: { 5021 setTablesAndProjectionMapForData(qb, uri, projection, false); 5022 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"); 5023 if (uri.getPathSegments().size() > 2) { 5024 String email = uri.getLastPathSegment(); 5025 String address = mDbHelper.extractAddressFromEmailAddress(email); 5026 selectionArgs = insertSelectionArg(selectionArgs, address); 5027 qb.appendWhere(" AND UPPER(" + Email.DATA + ")=UPPER(?)"); 5028 } 5029 break; 5030 } 5031 5032 case EMAILS_FILTER: { 5033 String typeParam = uri.getQueryParameter(DataUsageFeedback.USAGE_TYPE); 5034 Integer typeInt = sDataUsageTypeMap.get(typeParam); 5035 if (typeInt == null) { 5036 typeInt = DataUsageStatColumns.USAGE_TYPE_INT_LONG_TEXT; 5037 } 5038 setTablesAndProjectionMapForData(qb, uri, projection, true, typeInt); 5039 String filterParam = null; 5040 5041 if (uri.getPathSegments().size() > 3) { 5042 filterParam = uri.getLastPathSegment(); 5043 if (TextUtils.isEmpty(filterParam)) { 5044 filterParam = null; 5045 } 5046 } 5047 5048 if (filterParam == null) { 5049 // If the filter is unspecified, return nothing 5050 qb.appendWhere(" AND 0"); 5051 } else { 5052 StringBuilder sb = new StringBuilder(); 5053 sb.append(" AND " + Data._ID + " IN ("); 5054 sb.append( 5055 "SELECT " + Data._ID + 5056 " FROM " + Tables.DATA + 5057 " WHERE " + DataColumns.MIMETYPE_ID + "="); 5058 sb.append(mDbHelper.getMimeTypeIdForEmail()); 5059 sb.append(" AND " + Data.DATA1 + " LIKE "); 5060 DatabaseUtils.appendEscapedSQLString(sb, filterParam + '%'); 5061 if (!filterParam.contains("@")) { 5062 sb.append( 5063 " UNION SELECT " + Data._ID + 5064 " FROM " + Tables.DATA + 5065 " WHERE +" + DataColumns.MIMETYPE_ID + "="); 5066 sb.append(mDbHelper.getMimeTypeIdForEmail()); 5067 sb.append(" AND " + Data.RAW_CONTACT_ID + " IN " + 5068 "(SELECT " + RawContactsColumns.CONCRETE_ID + 5069 " FROM " + Tables.SEARCH_INDEX + 5070 " JOIN " + Tables.RAW_CONTACTS + 5071 " ON (" + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID 5072 + "=" + RawContactsColumns.CONCRETE_CONTACT_ID + ")" + 5073 " WHERE " + SearchIndexColumns.NAME + " MATCH "); 5074 DatabaseUtils.appendEscapedSQLString(sb, sanitizeMatch(filterParam) + "*"); 5075 sb.append(")"); 5076 } 5077 sb.append(")"); 5078 qb.appendWhere(sb); 5079 } 5080 groupBy = Email.DATA + "," + RawContacts.CONTACT_ID; 5081 if (sortOrder == null) { 5082 final String accountPromotionSortOrder = getAccountPromotionSortOrder(uri); 5083 if (!TextUtils.isEmpty(accountPromotionSortOrder)) { 5084 sortOrder = accountPromotionSortOrder + ", " + EMAIL_FILTER_SORT_ORDER; 5085 } else { 5086 sortOrder = EMAIL_FILTER_SORT_ORDER; 5087 } 5088 } 5089 break; 5090 } 5091 5092 case POSTALS: { 5093 setTablesAndProjectionMapForData(qb, uri, projection, false); 5094 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" 5095 + StructuredPostal.CONTENT_ITEM_TYPE + "'"); 5096 break; 5097 } 5098 5099 case POSTALS_ID: { 5100 setTablesAndProjectionMapForData(qb, uri, projection, false); 5101 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 5102 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" 5103 + StructuredPostal.CONTENT_ITEM_TYPE + "'"); 5104 qb.appendWhere(" AND " + Data._ID + "=?"); 5105 break; 5106 } 5107 5108 case RAW_CONTACTS: { 5109 setTablesAndProjectionMapForRawContacts(qb, uri); 5110 profileRestrictionColumnName = RawContacts.RAW_CONTACT_IS_USER_PROFILE; 5111 break; 5112 } 5113 5114 case RAW_CONTACTS_ID: { 5115 long rawContactId = ContentUris.parseId(uri); 5116 enforceProfilePermissionForRawContact(db, rawContactId, false); 5117 setTablesAndProjectionMapForRawContacts(qb, uri); 5118 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 5119 qb.appendWhere(" AND " + RawContacts._ID + "=?"); 5120 break; 5121 } 5122 5123 case RAW_CONTACTS_DATA: { 5124 long rawContactId = Long.parseLong(uri.getPathSegments().get(1)); 5125 setTablesAndProjectionMapForData(qb, uri, projection, false); 5126 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 5127 qb.appendWhere(" AND " + Data.RAW_CONTACT_ID + "=?"); 5128 profileRestrictionColumnName = RawContacts.RAW_CONTACT_IS_USER_PROFILE; 5129 break; 5130 } 5131 5132 case RAW_CONTACTS_ID_STREAM_ITEMS: { 5133 long rawContactId = Long.parseLong(uri.getPathSegments().get(1)); 5134 enforceProfilePermissionForRawContact(db, rawContactId, false); 5135 setTablesAndProjectionMapForStreamItems(qb); 5136 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 5137 qb.appendWhere(StreamItems.RAW_CONTACT_ID + "=?"); 5138 break; 5139 } 5140 5141 case PROFILE_RAW_CONTACTS: { 5142 enforceProfilePermission(false); 5143 setTablesAndProjectionMapForRawContacts(qb, uri); 5144 qb.appendWhere(" AND " + RawContacts.RAW_CONTACT_IS_USER_PROFILE + "=1"); 5145 break; 5146 } 5147 5148 case PROFILE_RAW_CONTACTS_ID: { 5149 enforceProfilePermission(false); 5150 long rawContactId = ContentUris.parseId(uri); 5151 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 5152 setTablesAndProjectionMapForRawContacts(qb, uri); 5153 qb.appendWhere(" AND " + RawContacts.RAW_CONTACT_IS_USER_PROFILE + "=1 AND " 5154 + RawContacts._ID + "=?"); 5155 break; 5156 } 5157 5158 case PROFILE_RAW_CONTACTS_ID_DATA: { 5159 enforceProfilePermission(false); 5160 long rawContactId = Long.parseLong(uri.getPathSegments().get(2)); 5161 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 5162 setTablesAndProjectionMapForData(qb, uri, projection, false); 5163 qb.appendWhere(" AND " + RawContacts.RAW_CONTACT_IS_USER_PROFILE + "=1 AND " 5164 + Data.RAW_CONTACT_ID + "=?"); 5165 break; 5166 } 5167 5168 case PROFILE_RAW_CONTACTS_ID_ENTITIES: { 5169 enforceProfilePermission(false); 5170 long rawContactId = Long.parseLong(uri.getPathSegments().get(2)); 5171 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 5172 setTablesAndProjectionMapForRawEntities(qb, uri); 5173 qb.appendWhere(" AND " + RawContacts.RAW_CONTACT_IS_USER_PROFILE + "=1 AND " 5174 + RawContacts._ID + "=?"); 5175 break; 5176 } 5177 5178 case DATA: { 5179 setTablesAndProjectionMapForData(qb, uri, projection, false); 5180 profileRestrictionColumnName = RawContacts.RAW_CONTACT_IS_USER_PROFILE; 5181 break; 5182 } 5183 5184 case DATA_ID: { 5185 long dataId = ContentUris.parseId(uri); 5186 enforceProfilePermissionForData(db, dataId, false); 5187 setTablesAndProjectionMapForData(qb, uri, projection, false); 5188 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 5189 qb.appendWhere(" AND " + Data._ID + "=?"); 5190 break; 5191 } 5192 5193 case PHONE_LOOKUP: { 5194 5195 if (TextUtils.isEmpty(sortOrder)) { 5196 // Default the sort order to something reasonable so we get consistent 5197 // results when callers don't request an ordering 5198 sortOrder = " length(lookup.normalized_number) DESC"; 5199 } 5200 5201 String number = uri.getPathSegments().size() > 1 ? uri.getLastPathSegment() : ""; 5202 String numberE164 = PhoneNumberUtils.formatNumberToE164(number, 5203 mDbHelper.getCurrentCountryIso()); 5204 String normalizedNumber = 5205 PhoneNumberUtils.normalizeNumber(number); 5206 mDbHelper.buildPhoneLookupAndContactQuery(qb, normalizedNumber, numberE164); 5207 qb.setProjectionMap(sPhoneLookupProjectionMap); 5208 // Phone lookup cannot be combined with a selection 5209 selection = null; 5210 selectionArgs = null; 5211 break; 5212 } 5213 5214 case GROUPS: { 5215 qb.setTables(Views.GROUPS); 5216 qb.setProjectionMap(sGroupsProjectionMap); 5217 appendAccountFromParameter(qb, uri, true); 5218 break; 5219 } 5220 5221 case GROUPS_ID: { 5222 qb.setTables(Views.GROUPS); 5223 qb.setProjectionMap(sGroupsProjectionMap); 5224 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 5225 qb.appendWhere(Groups._ID + "=?"); 5226 break; 5227 } 5228 5229 case GROUPS_SUMMARY: { 5230 final boolean returnGroupCountPerAccount = 5231 readBooleanQueryParameter(uri, Groups.PARAM_RETURN_GROUP_COUNT_PER_ACCOUNT, 5232 false); 5233 qb.setTables(Views.GROUPS + " AS " + Tables.GROUPS); 5234 qb.setProjectionMap(returnGroupCountPerAccount ? 5235 sGroupsSummaryProjectionMapWithGroupCountPerAccount 5236 : sGroupsSummaryProjectionMap); 5237 appendAccountFromParameter(qb, uri, true); 5238 groupBy = GroupsColumns.CONCRETE_ID; 5239 break; 5240 } 5241 5242 case AGGREGATION_EXCEPTIONS: { 5243 qb.setTables(Tables.AGGREGATION_EXCEPTIONS); 5244 qb.setProjectionMap(sAggregationExceptionsProjectionMap); 5245 break; 5246 } 5247 5248 case AGGREGATION_SUGGESTIONS: { 5249 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 5250 String filter = null; 5251 if (uri.getPathSegments().size() > 3) { 5252 filter = uri.getPathSegments().get(3); 5253 } 5254 final int maxSuggestions; 5255 if (limit != null) { 5256 maxSuggestions = Integer.parseInt(limit); 5257 } else { 5258 maxSuggestions = DEFAULT_MAX_SUGGESTIONS; 5259 } 5260 5261 ArrayList<AggregationSuggestionParameter> parameters = null; 5262 List<String> query = uri.getQueryParameters("query"); 5263 if (query != null && !query.isEmpty()) { 5264 parameters = new ArrayList<AggregationSuggestionParameter>(query.size()); 5265 for (String parameter : query) { 5266 int offset = parameter.indexOf(':'); 5267 parameters.add(offset == -1 5268 ? new AggregationSuggestionParameter( 5269 AggregationSuggestions.PARAMETER_MATCH_NAME, 5270 parameter) 5271 : new AggregationSuggestionParameter( 5272 parameter.substring(0, offset), 5273 parameter.substring(offset + 1))); 5274 } 5275 } 5276 5277 setTablesAndProjectionMapForContacts(qb, uri, projection); 5278 5279 return mContactAggregator.queryAggregationSuggestions(qb, projection, contactId, 5280 maxSuggestions, filter, parameters); 5281 } 5282 5283 case SETTINGS: { 5284 qb.setTables(Tables.SETTINGS); 5285 qb.setProjectionMap(sSettingsProjectionMap); 5286 appendAccountFromParameter(qb, uri, false); 5287 5288 // When requesting specific columns, this query requires 5289 // late-binding of the GroupMembership MIME-type. 5290 final String groupMembershipMimetypeId = Long.toString(mDbHelper 5291 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)); 5292 if (projection != null && projection.length != 0 && 5293 mDbHelper.isInProjection(projection, Settings.UNGROUPED_COUNT)) { 5294 selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId); 5295 } 5296 if (projection != null && projection.length != 0 && 5297 mDbHelper.isInProjection(projection, Settings.UNGROUPED_WITH_PHONES)) { 5298 selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId); 5299 } 5300 5301 break; 5302 } 5303 5304 case STATUS_UPDATES: { 5305 setTableAndProjectionMapForStatusUpdates(qb, projection); 5306 break; 5307 } 5308 5309 case STATUS_UPDATES_ID: { 5310 setTableAndProjectionMapForStatusUpdates(qb, projection); 5311 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 5312 qb.appendWhere(DataColumns.CONCRETE_ID + "=?"); 5313 break; 5314 } 5315 5316 case SEARCH_SUGGESTIONS: { 5317 return mGlobalSearchSupport.handleSearchSuggestionsQuery( 5318 db, uri, projection, limit); 5319 } 5320 5321 case SEARCH_SHORTCUT: { 5322 String lookupKey = uri.getLastPathSegment(); 5323 String filter = getQueryParameter( 5324 uri, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA); 5325 return mGlobalSearchSupport.handleSearchShortcutRefresh( 5326 db, projection, lookupKey, filter); 5327 } 5328 5329 case LIVE_FOLDERS_CONTACTS: 5330 qb.setTables(Views.CONTACTS); 5331 qb.setProjectionMap(sLiveFoldersProjectionMap); 5332 break; 5333 5334 case LIVE_FOLDERS_CONTACTS_WITH_PHONES: 5335 qb.setTables(Views.CONTACTS); 5336 qb.setProjectionMap(sLiveFoldersProjectionMap); 5337 qb.appendWhere(Contacts.HAS_PHONE_NUMBER + "=1"); 5338 break; 5339 5340 case LIVE_FOLDERS_CONTACTS_FAVORITES: 5341 qb.setTables(Views.CONTACTS); 5342 qb.setProjectionMap(sLiveFoldersProjectionMap); 5343 qb.appendWhere(Contacts.STARRED + "=1"); 5344 break; 5345 5346 case LIVE_FOLDERS_CONTACTS_GROUP_NAME: 5347 qb.setTables(Views.CONTACTS); 5348 qb.setProjectionMap(sLiveFoldersProjectionMap); 5349 qb.appendWhere(CONTACTS_IN_GROUP_SELECT); 5350 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 5351 break; 5352 5353 case RAW_CONTACT_ENTITIES: { 5354 setTablesAndProjectionMapForRawEntities(qb, uri); 5355 break; 5356 } 5357 5358 case RAW_CONTACT_ENTITY_ID: { 5359 long rawContactId = Long.parseLong(uri.getPathSegments().get(1)); 5360 setTablesAndProjectionMapForRawEntities(qb, uri); 5361 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 5362 qb.appendWhere(" AND " + RawContacts._ID + "=?"); 5363 break; 5364 } 5365 5366 case PROVIDER_STATUS: { 5367 return queryProviderStatus(uri, projection); 5368 } 5369 5370 case DIRECTORIES : { 5371 qb.setTables(Tables.DIRECTORIES); 5372 qb.setProjectionMap(sDirectoryProjectionMap); 5373 break; 5374 } 5375 5376 case DIRECTORIES_ID : { 5377 long id = ContentUris.parseId(uri); 5378 qb.setTables(Tables.DIRECTORIES); 5379 qb.setProjectionMap(sDirectoryProjectionMap); 5380 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(id)); 5381 qb.appendWhere(Directory._ID + "=?"); 5382 break; 5383 } 5384 5385 case COMPLETE_NAME: { 5386 return completeName(uri, projection); 5387 } 5388 5389 default: 5390 return mLegacyApiSupport.query(uri, projection, selection, selectionArgs, 5391 sortOrder, limit); 5392 } 5393 5394 qb.setStrict(true); 5395 5396 if (profileRestrictionColumnName != null) { 5397 // This check is very slow and most of the rows will pass though this check, so 5398 // it should be put after user's selection, so SQLite won't do this check first. 5399 selection = appendProfileRestriction(uri, profileRestrictionColumnName, 5400 suppressProfileCheck, selection); 5401 } 5402 5403 Cursor cursor = 5404 query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit); 5405 if (readBooleanQueryParameter(uri, ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, false)) { 5406 cursor = bundleLetterCountExtras(cursor, db, qb, selection, selectionArgs, sortOrder); 5407 } 5408 return cursor; 5409 } 5410 5411 private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection, 5412 String selection, String[] selectionArgs, String sortOrder, String groupBy, 5413 String limit) { 5414 if (projection != null && projection.length == 1 5415 && BaseColumns._COUNT.equals(projection[0])) { 5416 qb.setProjectionMap(sCountProjectionMap); 5417 } 5418 final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null, 5419 sortOrder, limit); 5420 if (c != null) { 5421 c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI); 5422 } 5423 return c; 5424 } 5425 5426 /** 5427 * Creates a single-row cursor containing the current status of the provider. 5428 */ 5429 private Cursor queryProviderStatus(Uri uri, String[] projection) { 5430 MatrixCursor cursor = new MatrixCursor(projection); 5431 RowBuilder row = cursor.newRow(); 5432 for (int i = 0; i < projection.length; i++) { 5433 if (ProviderStatus.STATUS.equals(projection[i])) { 5434 row.add(mProviderStatus); 5435 } else if (ProviderStatus.DATA1.equals(projection[i])) { 5436 row.add(mEstimatedStorageRequirement); 5437 } 5438 } 5439 return cursor; 5440 } 5441 5442 /** 5443 * Runs the query with the supplied contact ID and lookup ID. If the query succeeds, 5444 * it returns the resulting cursor, otherwise it returns null and the calling 5445 * method needs to resolve the lookup key and rerun the query. 5446 */ 5447 private Cursor queryWithContactIdAndLookupKey(SQLiteQueryBuilder lookupQb, 5448 SQLiteDatabase db, Uri uri, 5449 String[] projection, String selection, String[] selectionArgs, 5450 String sortOrder, String groupBy, String limit, 5451 String contactIdColumn, long contactId, String lookupKeyColumn, String lookupKey) { 5452 String[] args; 5453 if (selectionArgs == null) { 5454 args = new String[2]; 5455 } else { 5456 args = new String[selectionArgs.length + 2]; 5457 System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length); 5458 } 5459 args[0] = String.valueOf(contactId); 5460 args[1] = Uri.encode(lookupKey); 5461 lookupQb.appendWhere(contactIdColumn + "=? AND " + lookupKeyColumn + "=?"); 5462 Cursor c = query(db, lookupQb, projection, selection, args, sortOrder, 5463 groupBy, limit); 5464 if (c.getCount() != 0) { 5465 return c; 5466 } 5467 5468 c.close(); 5469 return null; 5470 } 5471 5472 private static final class AddressBookIndexQuery { 5473 public static final String LETTER = "letter"; 5474 public static final String TITLE = "title"; 5475 public static final String COUNT = "count"; 5476 5477 public static final String[] COLUMNS = new String[] { 5478 LETTER, TITLE, COUNT 5479 }; 5480 5481 public static final int COLUMN_LETTER = 0; 5482 public static final int COLUMN_TITLE = 1; 5483 public static final int COLUMN_COUNT = 2; 5484 5485 // The first letter of the sort key column is what is used for the index headings, except 5486 // in the case of the user's profile, in which case it is empty. 5487 public static final String SECTION_HEADING_TEMPLATE = 5488 "(CASE WHEN %1$s=1 THEN '' ELSE SUBSTR(%2$s,1,1) END)"; 5489 5490 public static final String ORDER_BY = LETTER + " COLLATE " + PHONEBOOK_COLLATOR_NAME; 5491 } 5492 5493 /** 5494 * Computes counts by the address book index titles and adds the resulting tally 5495 * to the returned cursor as a bundle of extras. 5496 */ 5497 private Cursor bundleLetterCountExtras(Cursor cursor, final SQLiteDatabase db, 5498 SQLiteQueryBuilder qb, String selection, String[] selectionArgs, String sortOrder) { 5499 String sortKey; 5500 5501 // The sort order suffix could be something like "DESC". 5502 // We want to preserve it in the query even though we will change 5503 // the sort column itself. 5504 String sortOrderSuffix = ""; 5505 if (sortOrder != null) { 5506 5507 // If the sort order contains one of the "is_profile" columns, we need to strip it out 5508 // first. 5509 if (sortOrder.contains(Contacts.IS_USER_PROFILE) 5510 || sortOrder.contains(RawContacts.RAW_CONTACT_IS_USER_PROFILE)) { 5511 String[] splitOrderClauses = sortOrder.split(","); 5512 StringBuilder rejoinedClause = new StringBuilder(); 5513 for (String orderClause : splitOrderClauses) { 5514 if (!orderClause.contains(Contacts.IS_USER_PROFILE) 5515 && !orderClause.contains(RawContacts.RAW_CONTACT_IS_USER_PROFILE)) { 5516 if (rejoinedClause.length() > 0) { 5517 rejoinedClause.append(", "); 5518 } 5519 rejoinedClause.append(orderClause.trim()); 5520 } 5521 } 5522 sortOrder = rejoinedClause.toString(); 5523 } 5524 5525 int spaceIndex = sortOrder.indexOf(' '); 5526 if (spaceIndex != -1) { 5527 sortKey = sortOrder.substring(0, spaceIndex); 5528 sortOrderSuffix = sortOrder.substring(spaceIndex); 5529 } else { 5530 sortKey = sortOrder; 5531 } 5532 } else { 5533 sortKey = Contacts.SORT_KEY_PRIMARY; 5534 } 5535 5536 String locale = getLocale().toString(); 5537 HashMap<String, String> projectionMap = Maps.newHashMap(); 5538 5539 // The user profile column varies depending on the view. 5540 String profileColumn = qb.getTables().contains(Views.CONTACTS) 5541 ? Contacts.IS_USER_PROFILE 5542 : RawContacts.RAW_CONTACT_IS_USER_PROFILE; 5543 String sectionHeading = String.format( 5544 AddressBookIndexQuery.SECTION_HEADING_TEMPLATE, profileColumn, sortKey); 5545 projectionMap.put(AddressBookIndexQuery.LETTER, 5546 sectionHeading + " AS " + AddressBookIndexQuery.LETTER); 5547 5548 /** 5549 * Use the GET_PHONEBOOK_INDEX function, which is an android extension for SQLite3, 5550 * to map the first letter of the sort key to a character that is traditionally 5551 * used in phonebooks to represent that letter. For example, in Korean it will 5552 * be the first consonant in the letter; for Japanese it will be Hiragana rather 5553 * than Katakana. 5554 */ 5555 projectionMap.put(AddressBookIndexQuery.TITLE, 5556 "GET_PHONEBOOK_INDEX(" + sectionHeading + ",'" + locale + "')" 5557 + " AS " + AddressBookIndexQuery.TITLE); 5558 projectionMap.put(AddressBookIndexQuery.COUNT, 5559 "COUNT(" + Contacts._ID + ") AS " + AddressBookIndexQuery.COUNT); 5560 qb.setProjectionMap(projectionMap); 5561 5562 Cursor indexCursor = qb.query(db, AddressBookIndexQuery.COLUMNS, selection, selectionArgs, 5563 AddressBookIndexQuery.ORDER_BY, null /* having */, 5564 AddressBookIndexQuery.ORDER_BY + sortOrderSuffix); 5565 5566 try { 5567 int groupCount = indexCursor.getCount(); 5568 String titles[] = new String[groupCount]; 5569 int counts[] = new int[groupCount]; 5570 int indexCount = 0; 5571 String currentTitle = null; 5572 5573 // Since GET_PHONEBOOK_INDEX is a many-to-1 function, we may end up 5574 // with multiple entries for the same title. The following code 5575 // collapses those duplicates. 5576 for (int i = 0; i < groupCount; i++) { 5577 indexCursor.moveToNext(); 5578 String title = indexCursor.getString(AddressBookIndexQuery.COLUMN_TITLE); 5579 int count = indexCursor.getInt(AddressBookIndexQuery.COLUMN_COUNT); 5580 if (indexCount == 0 || !TextUtils.equals(title, currentTitle)) { 5581 titles[indexCount] = currentTitle = title; 5582 counts[indexCount] = count; 5583 indexCount++; 5584 } else { 5585 counts[indexCount - 1] += count; 5586 } 5587 } 5588 5589 if (indexCount < groupCount) { 5590 String[] newTitles = new String[indexCount]; 5591 System.arraycopy(titles, 0, newTitles, 0, indexCount); 5592 titles = newTitles; 5593 5594 int[] newCounts = new int[indexCount]; 5595 System.arraycopy(counts, 0, newCounts, 0, indexCount); 5596 counts = newCounts; 5597 } 5598 5599 return new AddressBookCursor((CrossProcessCursor) cursor, titles, counts); 5600 } finally { 5601 indexCursor.close(); 5602 } 5603 } 5604 5605 /** 5606 * Returns the contact Id for the contact identified by the lookupKey. 5607 * Robust against changes in the lookup key: if the key has changed, will 5608 * look up the contact by the raw contact IDs or name encoded in the lookup 5609 * key. 5610 */ 5611 public long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) { 5612 ContactLookupKey key = new ContactLookupKey(); 5613 ArrayList<LookupKeySegment> segments = key.parse(lookupKey); 5614 5615 long contactId = -1; 5616 if (lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_SOURCE_ID)) { 5617 contactId = lookupContactIdBySourceIds(db, segments); 5618 if (contactId != -1) { 5619 return contactId; 5620 } 5621 } 5622 5623 boolean hasRawContactIds = 5624 lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID); 5625 if (hasRawContactIds) { 5626 contactId = lookupContactIdByRawContactIds(db, segments); 5627 if (contactId != -1) { 5628 return contactId; 5629 } 5630 } 5631 5632 if (hasRawContactIds 5633 || lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME)) { 5634 contactId = lookupContactIdByDisplayNames(db, segments); 5635 } 5636 5637 return contactId; 5638 } 5639 5640 private interface LookupBySourceIdQuery { 5641 String TABLE = Views.RAW_CONTACTS; 5642 5643 String COLUMNS[] = { 5644 RawContacts.CONTACT_ID, 5645 RawContacts.ACCOUNT_TYPE_AND_DATA_SET, 5646 RawContacts.ACCOUNT_NAME, 5647 RawContacts.SOURCE_ID 5648 }; 5649 5650 int CONTACT_ID = 0; 5651 int ACCOUNT_TYPE_AND_DATA_SET = 1; 5652 int ACCOUNT_NAME = 2; 5653 int SOURCE_ID = 3; 5654 } 5655 5656 private long lookupContactIdBySourceIds(SQLiteDatabase db, 5657 ArrayList<LookupKeySegment> segments) { 5658 StringBuilder sb = new StringBuilder(); 5659 sb.append(RawContacts.SOURCE_ID + " IN ("); 5660 for (int i = 0; i < segments.size(); i++) { 5661 LookupKeySegment segment = segments.get(i); 5662 if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID) { 5663 DatabaseUtils.appendEscapedSQLString(sb, segment.key); 5664 sb.append(","); 5665 } 5666 } 5667 sb.setLength(sb.length() - 1); // Last comma 5668 sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL"); 5669 5670 Cursor c = db.query(LookupBySourceIdQuery.TABLE, LookupBySourceIdQuery.COLUMNS, 5671 sb.toString(), null, null, null, null); 5672 try { 5673 while (c.moveToNext()) { 5674 String accountTypeAndDataSet = 5675 c.getString(LookupBySourceIdQuery.ACCOUNT_TYPE_AND_DATA_SET); 5676 String accountName = c.getString(LookupBySourceIdQuery.ACCOUNT_NAME); 5677 int accountHashCode = 5678 ContactLookupKey.getAccountHashCode(accountTypeAndDataSet, accountName); 5679 String sourceId = c.getString(LookupBySourceIdQuery.SOURCE_ID); 5680 for (int i = 0; i < segments.size(); i++) { 5681 LookupKeySegment segment = segments.get(i); 5682 if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID 5683 && accountHashCode == segment.accountHashCode 5684 && segment.key.equals(sourceId)) { 5685 segment.contactId = c.getLong(LookupBySourceIdQuery.CONTACT_ID); 5686 break; 5687 } 5688 } 5689 } 5690 } finally { 5691 c.close(); 5692 } 5693 5694 return getMostReferencedContactId(segments); 5695 } 5696 5697 private interface LookupByRawContactIdQuery { 5698 String TABLE = Views.RAW_CONTACTS; 5699 5700 String COLUMNS[] = { 5701 RawContacts.CONTACT_ID, 5702 RawContacts.ACCOUNT_TYPE_AND_DATA_SET, 5703 RawContacts.ACCOUNT_NAME, 5704 RawContacts._ID, 5705 }; 5706 5707 int CONTACT_ID = 0; 5708 int ACCOUNT_TYPE_AND_DATA_SET = 1; 5709 int ACCOUNT_NAME = 2; 5710 int ID = 3; 5711 } 5712 5713 private long lookupContactIdByRawContactIds(SQLiteDatabase db, 5714 ArrayList<LookupKeySegment> segments) { 5715 StringBuilder sb = new StringBuilder(); 5716 sb.append(RawContacts._ID + " IN ("); 5717 for (int i = 0; i < segments.size(); i++) { 5718 LookupKeySegment segment = segments.get(i); 5719 if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) { 5720 sb.append(segment.rawContactId); 5721 sb.append(","); 5722 } 5723 } 5724 sb.setLength(sb.length() - 1); // Last comma 5725 sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL"); 5726 5727 Cursor c = db.query(LookupByRawContactIdQuery.TABLE, LookupByRawContactIdQuery.COLUMNS, 5728 sb.toString(), null, null, null, null); 5729 try { 5730 while (c.moveToNext()) { 5731 String accountTypeAndDataSet = c.getString( 5732 LookupByRawContactIdQuery.ACCOUNT_TYPE_AND_DATA_SET); 5733 String accountName = c.getString(LookupByRawContactIdQuery.ACCOUNT_NAME); 5734 int accountHashCode = 5735 ContactLookupKey.getAccountHashCode(accountTypeAndDataSet, accountName); 5736 String rawContactId = c.getString(LookupByRawContactIdQuery.ID); 5737 for (int i = 0; i < segments.size(); i++) { 5738 LookupKeySegment segment = segments.get(i); 5739 if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID 5740 && accountHashCode == segment.accountHashCode 5741 && segment.rawContactId.equals(rawContactId)) { 5742 segment.contactId = c.getLong(LookupByRawContactIdQuery.CONTACT_ID); 5743 break; 5744 } 5745 } 5746 } 5747 } finally { 5748 c.close(); 5749 } 5750 5751 return getMostReferencedContactId(segments); 5752 } 5753 5754 private interface LookupByDisplayNameQuery { 5755 String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS; 5756 5757 String COLUMNS[] = { 5758 RawContacts.CONTACT_ID, 5759 RawContacts.ACCOUNT_TYPE_AND_DATA_SET, 5760 RawContacts.ACCOUNT_NAME, 5761 NameLookupColumns.NORMALIZED_NAME 5762 }; 5763 5764 int CONTACT_ID = 0; 5765 int ACCOUNT_TYPE_AND_DATA_SET = 1; 5766 int ACCOUNT_NAME = 2; 5767 int NORMALIZED_NAME = 3; 5768 } 5769 5770 private long lookupContactIdByDisplayNames(SQLiteDatabase db, 5771 ArrayList<LookupKeySegment> segments) { 5772 StringBuilder sb = new StringBuilder(); 5773 sb.append(NameLookupColumns.NORMALIZED_NAME + " IN ("); 5774 for (int i = 0; i < segments.size(); i++) { 5775 LookupKeySegment segment = segments.get(i); 5776 if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME 5777 || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) { 5778 DatabaseUtils.appendEscapedSQLString(sb, segment.key); 5779 sb.append(","); 5780 } 5781 } 5782 sb.setLength(sb.length() - 1); // Last comma 5783 sb.append(") AND " + NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NAME_COLLATION_KEY 5784 + " AND " + RawContacts.CONTACT_ID + " NOT NULL"); 5785 5786 Cursor c = db.query(LookupByDisplayNameQuery.TABLE, LookupByDisplayNameQuery.COLUMNS, 5787 sb.toString(), null, null, null, null); 5788 try { 5789 while (c.moveToNext()) { 5790 String accountTypeAndDataSet = 5791 c.getString(LookupByDisplayNameQuery.ACCOUNT_TYPE_AND_DATA_SET); 5792 String accountName = c.getString(LookupByDisplayNameQuery.ACCOUNT_NAME); 5793 int accountHashCode = 5794 ContactLookupKey.getAccountHashCode(accountTypeAndDataSet, accountName); 5795 String name = c.getString(LookupByDisplayNameQuery.NORMALIZED_NAME); 5796 for (int i = 0; i < segments.size(); i++) { 5797 LookupKeySegment segment = segments.get(i); 5798 if ((segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME 5799 || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) 5800 && accountHashCode == segment.accountHashCode 5801 && segment.key.equals(name)) { 5802 segment.contactId = c.getLong(LookupByDisplayNameQuery.CONTACT_ID); 5803 break; 5804 } 5805 } 5806 } 5807 } finally { 5808 c.close(); 5809 } 5810 5811 return getMostReferencedContactId(segments); 5812 } 5813 5814 private boolean lookupKeyContainsType(ArrayList<LookupKeySegment> segments, int lookupType) { 5815 for (int i = 0; i < segments.size(); i++) { 5816 LookupKeySegment segment = segments.get(i); 5817 if (segment.lookupType == lookupType) { 5818 return true; 5819 } 5820 } 5821 5822 return false; 5823 } 5824 5825 public void updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId) { 5826 mContactAggregator.updateLookupKeyForRawContact(db, rawContactId); 5827 } 5828 5829 /** 5830 * Returns the contact ID that is mentioned the highest number of times. 5831 */ 5832 private long getMostReferencedContactId(ArrayList<LookupKeySegment> segments) { 5833 Collections.sort(segments); 5834 5835 long bestContactId = -1; 5836 int bestRefCount = 0; 5837 5838 long contactId = -1; 5839 int count = 0; 5840 5841 int segmentCount = segments.size(); 5842 for (int i = 0; i < segmentCount; i++) { 5843 LookupKeySegment segment = segments.get(i); 5844 if (segment.contactId != -1) { 5845 if (segment.contactId == contactId) { 5846 count++; 5847 } else { 5848 if (count > bestRefCount) { 5849 bestContactId = contactId; 5850 bestRefCount = count; 5851 } 5852 contactId = segment.contactId; 5853 count = 1; 5854 } 5855 } 5856 } 5857 if (count > bestRefCount) { 5858 return contactId; 5859 } else { 5860 return bestContactId; 5861 } 5862 } 5863 5864 private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri, 5865 String[] projection) { 5866 setTablesAndProjectionMapForContacts(qb, uri, projection, false); 5867 } 5868 5869 /** 5870 * @param includeDataUsageStat true when the table should include DataUsageStat table. 5871 * Note that this uses INNER JOIN instead of LEFT OUTER JOIN, so some of data in Contacts 5872 * may be dropped. 5873 */ 5874 private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri, 5875 String[] projection, boolean includeDataUsageStat) { 5876 StringBuilder sb = new StringBuilder(); 5877 sb.append(Views.CONTACTS); 5878 5879 // Just for frequently contacted contacts in Strequent Uri handling. 5880 if (includeDataUsageStat) { 5881 sb.append(" INNER JOIN " + 5882 Views.DATA_USAGE_STAT + " AS " + Tables.DATA_USAGE_STAT + 5883 " ON (" + 5884 DbQueryUtils.concatenateClauses( 5885 DataUsageStatColumns.CONCRETE_TIMES_USED + " > 0", 5886 RawContacts.CONTACT_ID + "=" + Views.CONTACTS + "." + Contacts._ID) + 5887 ")"); 5888 } 5889 5890 appendContactPresenceJoin(sb, projection, Contacts._ID); 5891 appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID); 5892 qb.setTables(sb.toString()); 5893 qb.setProjectionMap(sContactsProjectionMap); 5894 } 5895 5896 /** 5897 * Finds name lookup records matching the supplied filter, picks one arbitrary match per 5898 * contact and joins that with other contacts tables. 5899 */ 5900 private void setTablesAndProjectionMapForContactsWithSnippet(SQLiteQueryBuilder qb, Uri uri, 5901 String[] projection, String filter, long directoryId) { 5902 5903 StringBuilder sb = new StringBuilder(); 5904 sb.append(Views.CONTACTS); 5905 5906 if (filter != null) { 5907 filter = filter.trim(); 5908 } 5909 5910 if (TextUtils.isEmpty(filter) || (directoryId != -1 && directoryId != Directory.DEFAULT)) { 5911 sb.append(" JOIN (SELECT NULL AS " + SearchSnippetColumns.SNIPPET + " WHERE 0)"); 5912 } else { 5913 appendSearchIndexJoin(sb, uri, projection, filter); 5914 } 5915 appendContactPresenceJoin(sb, projection, Contacts._ID); 5916 appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID); 5917 qb.setTables(sb.toString()); 5918 qb.setProjectionMap(sContactsProjectionWithSnippetMap); 5919 } 5920 5921 private void appendSearchIndexJoin( 5922 StringBuilder sb, Uri uri, String[] projection, String filter) { 5923 5924 if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET)) { 5925 String[] args = null; 5926 String snippetArgs = 5927 getQueryParameter(uri, SearchSnippetColumns.SNIPPET_ARGS_PARAM_KEY); 5928 if (snippetArgs != null) { 5929 args = snippetArgs.split(","); 5930 } 5931 5932 String startMatch = args != null && args.length > 0 ? args[0] 5933 : DEFAULT_SNIPPET_ARG_START_MATCH; 5934 String endMatch = args != null && args.length > 1 ? args[1] 5935 : DEFAULT_SNIPPET_ARG_END_MATCH; 5936 String ellipsis = args != null && args.length > 2 ? args[2] 5937 : DEFAULT_SNIPPET_ARG_ELLIPSIS; 5938 int maxTokens = args != null && args.length > 3 ? Integer.parseInt(args[3]) 5939 : DEFAULT_SNIPPET_ARG_MAX_TOKENS; 5940 5941 appendSearchIndexJoin( 5942 sb, filter, true, startMatch, endMatch, ellipsis, maxTokens); 5943 } else { 5944 appendSearchIndexJoin(sb, filter, false, null, null, null, 0); 5945 } 5946 } 5947 5948 public void appendSearchIndexJoin(StringBuilder sb, String filter, 5949 boolean snippetNeeded, String startMatch, String endMatch, String ellipsis, 5950 int maxTokens) { 5951 boolean isEmailAddress = false; 5952 String emailAddress = null; 5953 boolean isPhoneNumber = false; 5954 String phoneNumber = null; 5955 String numberE164 = null; 5956 5957 // If the query consists of a single word, we can do snippetizing after-the-fact for a 5958 // performance boost. 5959 boolean singleTokenSearch = filter.split(QUERY_TOKENIZER_REGEX).length == 1; 5960 5961 if (filter.indexOf('@') != -1) { 5962 emailAddress = mDbHelper.extractAddressFromEmailAddress(filter); 5963 isEmailAddress = !TextUtils.isEmpty(emailAddress); 5964 } else { 5965 isPhoneNumber = isPhoneNumber(filter); 5966 if (isPhoneNumber) { 5967 phoneNumber = PhoneNumberUtils.normalizeNumber(filter); 5968 numberE164 = PhoneNumberUtils.formatNumberToE164(phoneNumber, 5969 mDbHelper.getCountryIso()); 5970 } 5971 } 5972 5973 sb.append(" JOIN (SELECT " + SearchIndexColumns.CONTACT_ID + " AS snippet_contact_id"); 5974 if (snippetNeeded) { 5975 sb.append(", "); 5976 if (isEmailAddress) { 5977 sb.append("ifnull("); 5978 DatabaseUtils.appendEscapedSQLString(sb, startMatch); 5979 sb.append("||(SELECT MIN(" + Email.ADDRESS + ")"); 5980 sb.append(" FROM " + Tables.DATA_JOIN_RAW_CONTACTS); 5981 sb.append(" WHERE " + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID); 5982 sb.append("=" + RawContacts.CONTACT_ID + " AND " + Email.ADDRESS + " LIKE "); 5983 DatabaseUtils.appendEscapedSQLString(sb, filter + "%"); 5984 sb.append(")||"); 5985 DatabaseUtils.appendEscapedSQLString(sb, endMatch); 5986 sb.append(","); 5987 5988 // Optimization for single-token search. 5989 if (singleTokenSearch) { 5990 sb.append(SearchIndexColumns.CONTENT); 5991 } else { 5992 appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens); 5993 } 5994 sb.append(")"); 5995 } else if (isPhoneNumber) { 5996 sb.append("ifnull("); 5997 DatabaseUtils.appendEscapedSQLString(sb, startMatch); 5998 sb.append("||(SELECT MIN(" + Phone.NUMBER + ")"); 5999 sb.append(" FROM " + 6000 Tables.DATA_JOIN_RAW_CONTACTS + " JOIN " + Tables.PHONE_LOOKUP); 6001 sb.append(" ON " + DataColumns.CONCRETE_ID); 6002 sb.append("=" + Tables.PHONE_LOOKUP + "." + PhoneLookupColumns.DATA_ID); 6003 sb.append(" WHERE " + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID); 6004 sb.append("=" + RawContacts.CONTACT_ID); 6005 sb.append(" AND " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '"); 6006 sb.append(phoneNumber); 6007 sb.append("%'"); 6008 if (!TextUtils.isEmpty(numberE164)) { 6009 sb.append(" OR " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '"); 6010 sb.append(numberE164); 6011 sb.append("%'"); 6012 } 6013 sb.append(")||"); 6014 DatabaseUtils.appendEscapedSQLString(sb, endMatch); 6015 sb.append(","); 6016 6017 // Optimization for single-token search. 6018 if (singleTokenSearch) { 6019 sb.append(SearchIndexColumns.CONTENT); 6020 } else { 6021 appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens); 6022 } 6023 sb.append(")"); 6024 } else { 6025 final String normalizedFilter = NameNormalizer.normalize(filter); 6026 if (!TextUtils.isEmpty(normalizedFilter)) { 6027 // Optimization for single-token search. 6028 if (singleTokenSearch) { 6029 sb.append(SearchIndexColumns.CONTENT); 6030 } else { 6031 sb.append("(CASE WHEN EXISTS (SELECT 1 FROM "); 6032 sb.append(Tables.RAW_CONTACTS + " AS rc INNER JOIN "); 6033 sb.append(Tables.NAME_LOOKUP + " AS nl ON (rc." + RawContacts._ID); 6034 sb.append("=nl." + NameLookupColumns.RAW_CONTACT_ID); 6035 sb.append(") WHERE nl." + NameLookupColumns.NORMALIZED_NAME); 6036 sb.append(" GLOB '" + normalizedFilter + "*' AND "); 6037 sb.append("nl." + NameLookupColumns.NAME_TYPE + "="); 6038 sb.append(NameLookupType.NAME_COLLATION_KEY + " AND "); 6039 sb.append(Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID); 6040 sb.append("=rc." + RawContacts.CONTACT_ID); 6041 sb.append(") THEN NULL ELSE "); 6042 appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens); 6043 sb.append(" END)"); 6044 } 6045 } else { 6046 sb.append("NULL"); 6047 } 6048 } 6049 sb.append(" AS " + SearchSnippetColumns.SNIPPET); 6050 } 6051 6052 sb.append(" FROM " + Tables.SEARCH_INDEX); 6053 sb.append(" WHERE "); 6054 sb.append(Tables.SEARCH_INDEX + " MATCH "); 6055 if (isEmailAddress) { 6056 DatabaseUtils.appendEscapedSQLString(sb, "\"" + sanitizeMatch(filter) + "*\""); 6057 } else if (isPhoneNumber) { 6058 DatabaseUtils.appendEscapedSQLString(sb, 6059 "\"" + sanitizeMatch(filter) + "*\" OR \"" + phoneNumber + "*\"" 6060 + (numberE164 != null ? " OR \"" + numberE164 + "\"" : "")); 6061 } else { 6062 DatabaseUtils.appendEscapedSQLString(sb, sanitizeMatch(filter) + "*"); 6063 } 6064 sb.append(") ON (" + Contacts._ID + "=snippet_contact_id)"); 6065 } 6066 6067 private String sanitizeMatch(String filter) { 6068 // TODO more robust preprocessing of match expressions 6069 return filter.replace('-', ' ').replace('\"', ' '); 6070 } 6071 6072 private void appendSnippetFunction( 6073 StringBuilder sb, String startMatch, String endMatch, String ellipsis, int maxTokens) { 6074 sb.append("snippet(" + Tables.SEARCH_INDEX + ","); 6075 DatabaseUtils.appendEscapedSQLString(sb, startMatch); 6076 sb.append(","); 6077 DatabaseUtils.appendEscapedSQLString(sb, endMatch); 6078 sb.append(","); 6079 DatabaseUtils.appendEscapedSQLString(sb, ellipsis); 6080 6081 // The index of the column used for the snippet, "content" 6082 sb.append(",1,"); 6083 sb.append(maxTokens); 6084 sb.append(")"); 6085 } 6086 6087 private void setTablesAndProjectionMapForRawContacts(SQLiteQueryBuilder qb, Uri uri) { 6088 StringBuilder sb = new StringBuilder(); 6089 sb.append(Views.RAW_CONTACTS); 6090 qb.setTables(sb.toString()); 6091 qb.setProjectionMap(sRawContactsProjectionMap); 6092 appendAccountFromParameter(qb, uri, true); 6093 } 6094 6095 private void setTablesAndProjectionMapForRawEntities(SQLiteQueryBuilder qb, Uri uri) { 6096 qb.setTables(Views.RAW_ENTITIES); 6097 qb.setProjectionMap(sRawEntityProjectionMap); 6098 appendAccountFromParameter(qb, uri, true); 6099 } 6100 6101 private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri, 6102 String[] projection, boolean distinct) { 6103 setTablesAndProjectionMapForData(qb, uri, projection, distinct, null); 6104 } 6105 6106 /** 6107 * @param usageType when non-null {@link Tables#DATA_USAGE_STAT} is joined with the specified 6108 * type. 6109 */ 6110 private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri, 6111 String[] projection, boolean distinct, Integer usageType) { 6112 StringBuilder sb = new StringBuilder(); 6113 sb.append(Views.DATA); 6114 sb.append(" data"); 6115 6116 appendContactPresenceJoin(sb, projection, RawContacts.CONTACT_ID); 6117 appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID); 6118 appendDataPresenceJoin(sb, projection, DataColumns.CONCRETE_ID); 6119 appendDataStatusUpdateJoin(sb, projection, DataColumns.CONCRETE_ID); 6120 6121 if (usageType != null) { 6122 appendDataUsageStatJoin(sb, usageType, DataColumns.CONCRETE_ID); 6123 } 6124 6125 qb.setTables(sb.toString()); 6126 6127 boolean useDistinct = distinct 6128 || !mDbHelper.isInProjection(projection, DISTINCT_DATA_PROHIBITING_COLUMNS); 6129 qb.setDistinct(useDistinct); 6130 qb.setProjectionMap(useDistinct ? sDistinctDataProjectionMap : sDataProjectionMap); 6131 appendAccountFromParameter(qb, uri, true); 6132 } 6133 6134 private void setTableAndProjectionMapForStatusUpdates(SQLiteQueryBuilder qb, 6135 String[] projection) { 6136 StringBuilder sb = new StringBuilder(); 6137 sb.append(Views.DATA); 6138 sb.append(" data"); 6139 appendDataPresenceJoin(sb, projection, DataColumns.CONCRETE_ID); 6140 appendDataStatusUpdateJoin(sb, projection, DataColumns.CONCRETE_ID); 6141 6142 qb.setTables(sb.toString()); 6143 qb.setProjectionMap(sStatusUpdatesProjectionMap); 6144 } 6145 6146 private void setTablesAndProjectionMapForStreamItems(SQLiteQueryBuilder qb) { 6147 qb.setTables(Tables.STREAM_ITEMS 6148 + " JOIN " + Tables.RAW_CONTACTS + " ON (" 6149 + StreamItemsColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID 6150 + ") JOIN " + Tables.CONTACTS + " ON (" 6151 + RawContactsColumns.CONCRETE_CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")"); 6152 qb.setProjectionMap(sStreamItemsProjectionMap); 6153 } 6154 6155 private void setTablesAndProjectionMapForStreamItemPhotos(SQLiteQueryBuilder qb) { 6156 qb.setTables(Tables.PHOTO_FILES 6157 + " JOIN " + Tables.STREAM_ITEM_PHOTOS + " ON (" 6158 + StreamItemPhotosColumns.CONCRETE_PHOTO_FILE_ID + "=" 6159 + PhotoFilesColumns.CONCRETE_ID 6160 + ") JOIN " + Tables.STREAM_ITEMS + " ON (" 6161 + StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID + "=" 6162 + StreamItemsColumns.CONCRETE_ID + ")"); 6163 qb.setProjectionMap(sStreamItemPhotosProjectionMap); 6164 } 6165 6166 private void setTablesAndProjectionMapForEntities(SQLiteQueryBuilder qb, Uri uri, 6167 String[] projection) { 6168 StringBuilder sb = new StringBuilder(); 6169 sb.append(Views.ENTITIES); 6170 sb.append(" data"); 6171 6172 appendContactPresenceJoin(sb, projection, Contacts.Entity.CONTACT_ID); 6173 appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID); 6174 appendDataPresenceJoin(sb, projection, Contacts.Entity.DATA_ID); 6175 appendDataStatusUpdateJoin(sb, projection, Contacts.Entity.DATA_ID); 6176 6177 qb.setTables(sb.toString()); 6178 qb.setProjectionMap(sEntityProjectionMap); 6179 appendAccountFromParameter(qb, uri, true); 6180 } 6181 6182 private void appendContactStatusUpdateJoin(StringBuilder sb, String[] projection, 6183 String lastStatusUpdateIdColumn) { 6184 if (mDbHelper.isInProjection(projection, 6185 Contacts.CONTACT_STATUS, 6186 Contacts.CONTACT_STATUS_RES_PACKAGE, 6187 Contacts.CONTACT_STATUS_ICON, 6188 Contacts.CONTACT_STATUS_LABEL, 6189 Contacts.CONTACT_STATUS_TIMESTAMP)) { 6190 sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " " 6191 + ContactsStatusUpdatesColumns.ALIAS + 6192 " ON (" + lastStatusUpdateIdColumn + "=" 6193 + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")"); 6194 } 6195 } 6196 6197 private void appendDataStatusUpdateJoin(StringBuilder sb, String[] projection, 6198 String dataIdColumn) { 6199 if (mDbHelper.isInProjection(projection, 6200 StatusUpdates.STATUS, 6201 StatusUpdates.STATUS_RES_PACKAGE, 6202 StatusUpdates.STATUS_ICON, 6203 StatusUpdates.STATUS_LABEL, 6204 StatusUpdates.STATUS_TIMESTAMP)) { 6205 sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + 6206 " ON (" + StatusUpdatesColumns.CONCRETE_DATA_ID + "=" 6207 + dataIdColumn + ")"); 6208 } 6209 } 6210 6211 private void appendDataUsageStatJoin(StringBuilder sb, int usageType, String dataIdColumn) { 6212 sb.append(" LEFT OUTER JOIN " + Tables.DATA_USAGE_STAT + 6213 " ON (" + DataUsageStatColumns.CONCRETE_DATA_ID + "=" + dataIdColumn + 6214 " AND " + DataUsageStatColumns.CONCRETE_USAGE_TYPE + "=" + usageType + ")"); 6215 } 6216 6217 private void appendContactPresenceJoin(StringBuilder sb, String[] projection, 6218 String contactIdColumn) { 6219 if (mDbHelper.isInProjection(projection, 6220 Contacts.CONTACT_PRESENCE, Contacts.CONTACT_CHAT_CAPABILITY)) { 6221 sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE + 6222 " ON (" + contactIdColumn + " = " 6223 + AggregatedPresenceColumns.CONCRETE_CONTACT_ID + ")"); 6224 } 6225 } 6226 6227 private void appendDataPresenceJoin(StringBuilder sb, String[] projection, 6228 String dataIdColumn) { 6229 if (mDbHelper.isInProjection(projection, Data.PRESENCE, Data.CHAT_CAPABILITY)) { 6230 sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE + 6231 " ON (" + StatusUpdates.DATA_ID + "=" + dataIdColumn + ")"); 6232 } 6233 } 6234 6235 private boolean appendLocalDirectorySelectionIfNeeded(SQLiteQueryBuilder qb, long directoryId) { 6236 if (directoryId == Directory.DEFAULT) { 6237 qb.appendWhere(Contacts._ID + " IN " + Tables.DEFAULT_DIRECTORY); 6238 return true; 6239 } else if (directoryId == Directory.LOCAL_INVISIBLE){ 6240 qb.appendWhere(Contacts._ID + " NOT IN " + Tables.DEFAULT_DIRECTORY); 6241 return true; 6242 } 6243 return false; 6244 } 6245 6246 private String appendProfileRestriction(Uri uri, String profileColumn, 6247 boolean suppressProfileCheck, String originalSelection) { 6248 if (shouldIncludeProfile(uri, suppressProfileCheck)) { 6249 return originalSelection; 6250 } else { 6251 final String SELECTION = "(" + profileColumn + " IS NULL OR " + profileColumn + "=0)"; 6252 if (TextUtils.isEmpty(originalSelection)) { 6253 return SELECTION; 6254 } else { 6255 StringBuilder sb = new StringBuilder(); 6256 sb.append("("); 6257 sb.append(originalSelection); 6258 sb.append(") AND "); 6259 sb.append(SELECTION); 6260 return sb.toString(); 6261 } 6262 } 6263 } 6264 6265 private String prependProfileSortIfNeeded(Uri uri, String sortOrder, 6266 boolean suppressProfileCheck) { 6267 if (shouldIncludeProfile(uri, suppressProfileCheck)) { 6268 if (TextUtils.isEmpty(sortOrder)) { 6269 return Contacts.IS_USER_PROFILE + " DESC"; 6270 } else { 6271 return Contacts.IS_USER_PROFILE + " DESC, " + sortOrder; 6272 } 6273 } 6274 return sortOrder; 6275 } 6276 6277 private boolean shouldIncludeProfile(Uri uri, boolean suppressProfileCheck) { 6278 // The user's profile may be returned alongside other contacts if it was requested and 6279 // the calling application has permission to read profile data. 6280 boolean profileRequested = readBooleanQueryParameter(uri, ContactsContract.ALLOW_PROFILE, 6281 false); 6282 if (profileRequested && !suppressProfileCheck) { 6283 enforceProfilePermission(false); 6284 } 6285 return profileRequested; 6286 } 6287 6288 private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri, 6289 boolean includeDataSet) { 6290 final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME); 6291 final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE); 6292 final String dataSet = getQueryParameter(uri, RawContacts.DATA_SET); 6293 6294 final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType); 6295 if (partialUri) { 6296 // Throw when either account is incomplete 6297 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 6298 "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri)); 6299 } 6300 6301 // Accounts are valid by only checking one parameter, since we've 6302 // already ruled out partial accounts. 6303 final boolean validAccount = !TextUtils.isEmpty(accountName); 6304 if (validAccount) { 6305 String toAppend = RawContacts.ACCOUNT_NAME + "=" 6306 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 6307 + RawContacts.ACCOUNT_TYPE + "=" 6308 + DatabaseUtils.sqlEscapeString(accountType); 6309 if (includeDataSet) { 6310 if (dataSet == null) { 6311 toAppend += " AND " + RawContacts.DATA_SET + " IS NULL"; 6312 } else { 6313 toAppend += " AND " + RawContacts.DATA_SET + "=" + 6314 DatabaseUtils.sqlEscapeString(dataSet); 6315 } 6316 } 6317 qb.appendWhere(toAppend); 6318 } else { 6319 qb.appendWhere("1"); 6320 } 6321 } 6322 6323 private String appendAccountToSelection(Uri uri, String selection) { 6324 final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME); 6325 final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE); 6326 final String dataSet = getQueryParameter(uri, RawContacts.DATA_SET); 6327 6328 final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType); 6329 if (partialUri) { 6330 // Throw when either account is incomplete 6331 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 6332 "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri)); 6333 } 6334 6335 // Accounts are valid by only checking one parameter, since we've 6336 // already ruled out partial accounts. 6337 final boolean validAccount = !TextUtils.isEmpty(accountName); 6338 if (validAccount) { 6339 StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "=" 6340 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 6341 + RawContacts.ACCOUNT_TYPE + "=" 6342 + DatabaseUtils.sqlEscapeString(accountType)); 6343 if (!TextUtils.isEmpty(dataSet)) { 6344 selectionSb.append(" AND " + RawContacts.DATA_SET + "=") 6345 .append(DatabaseUtils.sqlEscapeString(dataSet)); 6346 } 6347 if (!TextUtils.isEmpty(selection)) { 6348 selectionSb.append(" AND ("); 6349 selectionSb.append(selection); 6350 selectionSb.append(')'); 6351 } 6352 return selectionSb.toString(); 6353 } else { 6354 return selection; 6355 } 6356 } 6357 6358 /** 6359 * Gets the value of the "limit" URI query parameter. 6360 * 6361 * @return A string containing a non-negative integer, or <code>null</code> if 6362 * the parameter is not set, or is set to an invalid value. 6363 */ 6364 private String getLimit(Uri uri) { 6365 String limitParam = getQueryParameter(uri, ContactsContract.LIMIT_PARAM_KEY); 6366 if (limitParam == null) { 6367 return null; 6368 } 6369 // make sure that the limit is a non-negative integer 6370 try { 6371 int l = Integer.parseInt(limitParam); 6372 if (l < 0) { 6373 Log.w(TAG, "Invalid limit parameter: " + limitParam); 6374 return null; 6375 } 6376 return String.valueOf(l); 6377 } catch (NumberFormatException ex) { 6378 Log.w(TAG, "Invalid limit parameter: " + limitParam); 6379 return null; 6380 } 6381 } 6382 6383 @Override 6384 public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException { 6385 6386 if (mode.equals("r")) { 6387 waitForAccess(mReadAccessLatch); 6388 } else { 6389 waitForAccess(mWriteAccessLatch); 6390 } 6391 6392 int match = sUriMatcher.match(uri); 6393 switch (match) { 6394 case CONTACTS_ID_PHOTO: { 6395 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 6396 long rawContactId = Long.parseLong(uri.getPathSegments().get(1)); 6397 enforceProfilePermissionForRawContact(db, rawContactId, false); 6398 return openPhotoAssetFile(db, uri, mode, 6399 Data._ID + "=" + Contacts.PHOTO_ID + " AND " + 6400 RawContacts.CONTACT_ID + "=?", 6401 new String[]{String.valueOf(rawContactId)}); 6402 } 6403 6404 case CONTACTS_ID_DISPLAY_PHOTO: { 6405 if (!mode.equals("r")) { 6406 throw new IllegalArgumentException( 6407 "Display photos retrieved by contact ID can only be read."); 6408 } 6409 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 6410 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 6411 enforceProfilePermissionForContact(db, contactId, false); 6412 Cursor c = db.query(Tables.CONTACTS, 6413 new String[]{Contacts.PHOTO_FILE_ID}, 6414 Contacts._ID + "=?", new String[]{String.valueOf(contactId)}, 6415 null, null, null); 6416 try { 6417 c.moveToFirst(); 6418 long photoFileId = c.getLong(0); 6419 return openDisplayPhotoForRead(photoFileId); 6420 } finally { 6421 c.close(); 6422 } 6423 } 6424 6425 case CONTACTS_LOOKUP_DISPLAY_PHOTO: 6426 case CONTACTS_LOOKUP_ID_DISPLAY_PHOTO: { 6427 if (!mode.equals("r")) { 6428 throw new IllegalArgumentException( 6429 "Display photos retrieved by contact lookup key can only be read."); 6430 } 6431 List<String> pathSegments = uri.getPathSegments(); 6432 int segmentCount = pathSegments.size(); 6433 if (segmentCount < 4) { 6434 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 6435 "Missing a lookup key", uri)); 6436 } 6437 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 6438 String lookupKey = pathSegments.get(2); 6439 String[] projection = new String[]{Contacts.PHOTO_FILE_ID}; 6440 if (segmentCount == 5) { 6441 long contactId = Long.parseLong(pathSegments.get(3)); 6442 enforceProfilePermissionForContact(db, contactId, false); 6443 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 6444 setTablesAndProjectionMapForContacts(lookupQb, uri, projection); 6445 Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri, 6446 projection, null, null, null, null, null, 6447 Contacts._ID, contactId, Contacts.LOOKUP_KEY, lookupKey); 6448 if (c != null) { 6449 try { 6450 c.moveToFirst(); 6451 long photoFileId = c.getLong(c.getColumnIndex(Contacts.PHOTO_FILE_ID)); 6452 return openDisplayPhotoForRead(photoFileId); 6453 } finally { 6454 c.close(); 6455 } 6456 } 6457 } 6458 6459 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 6460 setTablesAndProjectionMapForContacts(qb, uri, projection); 6461 long contactId = lookupContactIdByLookupKey(db, lookupKey); 6462 enforceProfilePermissionForContact(db, contactId, false); 6463 Cursor c = qb.query(db, projection, Contacts._ID + "=?", 6464 new String[]{String.valueOf(contactId)}, null, null, null); 6465 try { 6466 c.moveToFirst(); 6467 long photoFileId = c.getLong(c.getColumnIndex(Contacts.PHOTO_FILE_ID)); 6468 return openDisplayPhotoForRead(photoFileId); 6469 } finally { 6470 c.close(); 6471 } 6472 } 6473 6474 case RAW_CONTACTS_ID_DISPLAY_PHOTO: { 6475 long rawContactId = Long.parseLong(uri.getPathSegments().get(1)); 6476 boolean writeable = !mode.equals("r"); 6477 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 6478 enforceProfilePermissionForRawContact(db, rawContactId, writeable); 6479 6480 // Find the primary photo data record for this raw contact. 6481 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 6482 String[] projection = new String[]{Data._ID, Photo.PHOTO_FILE_ID}; 6483 setTablesAndProjectionMapForData(qb, uri, projection, false); 6484 Cursor c = qb.query(db, projection, 6485 Data.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", 6486 new String[]{String.valueOf(rawContactId), Photo.CONTENT_ITEM_TYPE}, 6487 null, null, Data.IS_PRIMARY + " DESC"); 6488 long dataId = 0; 6489 long photoFileId = 0; 6490 try { 6491 if (c.getCount() >= 1) { 6492 c.moveToFirst(); 6493 dataId = c.getLong(0); 6494 photoFileId = c.getLong(1); 6495 } 6496 } finally { 6497 c.close(); 6498 } 6499 6500 // If writeable, open a writeable file descriptor that we can monitor. 6501 // When the caller finishes writing content, we'll process the photo and 6502 // update the data record. 6503 if (writeable) { 6504 return openDisplayPhotoForWrite(rawContactId, dataId, uri, mode); 6505 } else { 6506 return openDisplayPhotoForRead(photoFileId); 6507 } 6508 } 6509 6510 case DISPLAY_PHOTO: { 6511 long photoFileId = ContentUris.parseId(uri); 6512 if (!mode.equals("r")) { 6513 throw new IllegalArgumentException( 6514 "Display photos retrieved by key can only be read."); 6515 } 6516 return openDisplayPhotoForRead(photoFileId); 6517 } 6518 6519 case DATA_ID: { 6520 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 6521 long dataId = Long.parseLong(uri.getPathSegments().get(1)); 6522 enforceProfilePermissionForData(db, dataId, false); 6523 return openPhotoAssetFile(db, uri, mode, 6524 Data._ID + "=? AND " + Data.MIMETYPE + "='" + Photo.CONTENT_ITEM_TYPE + "'", 6525 new String[]{String.valueOf(dataId)}); 6526 } 6527 6528 case PROFILE_AS_VCARD: { 6529 // When opening a contact as file, we pass back contents as a 6530 // vCard-encoded stream. We build into a local buffer first, 6531 // then pipe into MemoryFile once the exact size is known. 6532 final ByteArrayOutputStream localStream = new ByteArrayOutputStream(); 6533 outputRawContactsAsVCard(uri, localStream, null, null); 6534 return buildAssetFileDescriptor(localStream); 6535 } 6536 6537 case CONTACTS_AS_VCARD: { 6538 // When opening a contact as file, we pass back contents as a 6539 // vCard-encoded stream. We build into a local buffer first, 6540 // then pipe into MemoryFile once the exact size is known. 6541 final ByteArrayOutputStream localStream = new ByteArrayOutputStream(); 6542 outputRawContactsAsVCard(uri, localStream, null, null); 6543 return buildAssetFileDescriptor(localStream); 6544 } 6545 6546 case CONTACTS_AS_MULTI_VCARD: { 6547 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 6548 final String lookupKeys = uri.getPathSegments().get(2); 6549 final String[] loopupKeyList = lookupKeys.split(":"); 6550 final StringBuilder inBuilder = new StringBuilder(); 6551 Uri queryUri = Contacts.CONTENT_URI; 6552 int index = 0; 6553 6554 // SQLite has limits on how many parameters can be used 6555 // so the IDs are concatenated to a query string here instead 6556 for (String lookupKey : loopupKeyList) { 6557 if (index == 0) { 6558 inBuilder.append("("); 6559 } else { 6560 inBuilder.append(","); 6561 } 6562 long contactId = lookupContactIdByLookupKey(db, lookupKey); 6563 enforceProfilePermissionForContact(db, contactId, false); 6564 inBuilder.append(contactId); 6565 if (mProfileIdCache.profileContactId == contactId) { 6566 queryUri = queryUri.buildUpon().appendQueryParameter( 6567 ContactsContract.ALLOW_PROFILE, "true").build(); 6568 } 6569 index++; 6570 } 6571 inBuilder.append(')'); 6572 final String selection = Contacts._ID + " IN " + inBuilder.toString(); 6573 6574 // When opening a contact as file, we pass back contents as a 6575 // vCard-encoded stream. We build into a local buffer first, 6576 // then pipe into MemoryFile once the exact size is known. 6577 final ByteArrayOutputStream localStream = new ByteArrayOutputStream(); 6578 outputRawContactsAsVCard(queryUri, localStream, selection, null); 6579 return buildAssetFileDescriptor(localStream); 6580 } 6581 6582 default: 6583 throw new FileNotFoundException(mDbHelper.exceptionMessage("File does not exist", 6584 uri)); 6585 } 6586 } 6587 6588 private AssetFileDescriptor openPhotoAssetFile(SQLiteDatabase db, Uri uri, String mode, 6589 String selection, String[] selectionArgs) 6590 throws FileNotFoundException { 6591 if (!"r".equals(mode)) { 6592 throw new FileNotFoundException(mDbHelper.exceptionMessage("Mode " + mode 6593 + " not supported.", uri)); 6594 } 6595 6596 String sql = 6597 "SELECT " + Photo.PHOTO + " FROM " + Views.DATA + 6598 " WHERE " + selection; 6599 try { 6600 return makeAssetFileDescriptor( 6601 DatabaseUtils.blobFileDescriptorForQuery(db, sql, selectionArgs)); 6602 } catch (SQLiteDoneException e) { 6603 // this will happen if the DB query returns no rows (i.e. contact does not exist) 6604 throw new FileNotFoundException(uri.toString()); 6605 } 6606 } 6607 6608 /** 6609 * Opens a display photo from the photo store for reading. 6610 * @param photoFileId The display photo file ID 6611 * @return An asset file descriptor that allows the file to be read. 6612 * @throws FileNotFoundException If no photo file for the given ID exists. 6613 */ 6614 private AssetFileDescriptor openDisplayPhotoForRead(long photoFileId) 6615 throws FileNotFoundException { 6616 PhotoStore.Entry entry = mPhotoStore.get(photoFileId); 6617 if (entry != null) { 6618 return makeAssetFileDescriptor( 6619 ParcelFileDescriptor.open(new File(entry.path), 6620 ParcelFileDescriptor.MODE_READ_ONLY), 6621 entry.size); 6622 } else { 6623 scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS); 6624 throw new FileNotFoundException("No photo file found for ID " + photoFileId); 6625 } 6626 } 6627 6628 /** 6629 * Opens a file descriptor for a photo to be written. When the caller completes writing 6630 * to the file (closing the output stream), the image will be parsed out and processed. 6631 * If processing succeeds, the given raw contact ID's primary photo record will be 6632 * populated with the inserted image (if no primary photo record exists, the data ID can 6633 * be left as 0, and a new data record will be inserted). 6634 * @param rawContactId Raw contact ID this photo entry should be associated with. 6635 * @param dataId Data ID for a photo mimetype that will be updated with the inserted 6636 * image. May be set to 0, in which case the inserted image will trigger creation 6637 * of a new primary photo image data row for the raw contact. 6638 * @param uri The URI being used to access this file. 6639 * @param mode Read/write mode string. 6640 * @return An asset file descriptor the caller can use to write an image file for the 6641 * raw contact. 6642 */ 6643 private AssetFileDescriptor openDisplayPhotoForWrite(long rawContactId, long dataId, Uri uri, 6644 String mode) { 6645 try { 6646 return new AssetFileDescriptor(new MonitoredParcelFileDescriptor(rawContactId, dataId, 6647 ParcelFileDescriptor.open(File.createTempFile("img", null), 6648 ContentResolver.modeToMode(uri, mode))), 6649 0, AssetFileDescriptor.UNKNOWN_LENGTH); 6650 } catch (IOException ioe) { 6651 Log.e(TAG, "Could not create temp image file in mode " + mode); 6652 return null; 6653 } 6654 } 6655 6656 /** 6657 * Parcel file descriptor wrapper that monitors when the file is closed. 6658 * If the file contains a valid image, the image is either inserted into the given 6659 * raw contact or updated in the given data row. 6660 */ 6661 private class MonitoredParcelFileDescriptor extends ParcelFileDescriptor { 6662 private final long mRawContactId; 6663 private final long mDataId; 6664 private MonitoredParcelFileDescriptor(long rawContactId, long dataId, 6665 ParcelFileDescriptor descriptor) { 6666 super(descriptor); 6667 mRawContactId = rawContactId; 6668 mDataId = dataId; 6669 } 6670 6671 @Override 6672 public void close() throws IOException { 6673 try { 6674 // Check to see whether a valid image was written out. 6675 Bitmap b = BitmapFactory.decodeFileDescriptor(getFileDescriptor()); 6676 if (b != null) { 6677 PhotoProcessor processor = new PhotoProcessor(b, mMaxDisplayPhotoDim, 6678 mMaxThumbnailPhotoDim); 6679 6680 // Store the compressed photo in the photo store. 6681 long photoFileId = mPhotoStore.insert(processor); 6682 6683 // Depending on whether we already had a data row to attach the photo to, 6684 // do an update or insert. 6685 if (mDataId != 0) { 6686 // Update the data record with the new photo. 6687 ContentValues updateValues = new ContentValues(); 6688 6689 // Signal that photo processing has already been handled. 6690 updateValues.put(DataRowHandlerForPhoto.SKIP_PROCESSING_KEY, true); 6691 6692 if (photoFileId != 0) { 6693 updateValues.put(Photo.PHOTO_FILE_ID, photoFileId); 6694 } 6695 updateValues.put(Photo.PHOTO, processor.getThumbnailPhotoBytes()); 6696 update(ContentUris.withAppendedId(Data.CONTENT_URI, mDataId), updateValues, 6697 null, null); 6698 } else { 6699 // Insert a new primary data record with the photo. 6700 ContentValues insertValues = new ContentValues(); 6701 6702 // Signal that photo processing has already been handled. 6703 insertValues.put(DataRowHandlerForPhoto.SKIP_PROCESSING_KEY, true); 6704 6705 insertValues.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE); 6706 insertValues.put(Data.IS_PRIMARY, 1); 6707 if (photoFileId != 0) { 6708 insertValues.put(Photo.PHOTO_FILE_ID, photoFileId); 6709 } 6710 insertValues.put(Photo.PHOTO, processor.getThumbnailPhotoBytes()); 6711 insert(RawContacts.CONTENT_URI.buildUpon() 6712 .appendPath(String.valueOf(mRawContactId)) 6713 .appendPath(RawContacts.Data.CONTENT_DIRECTORY).build(), 6714 insertValues); 6715 } 6716 } 6717 } finally { 6718 super.close(); 6719 } 6720 } 6721 } 6722 6723 private static final String CONTACT_MEMORY_FILE_NAME = "contactAssetFile"; 6724 6725 /** 6726 * Returns an {@link AssetFileDescriptor} backed by the 6727 * contents of the given {@link ByteArrayOutputStream}. 6728 */ 6729 private AssetFileDescriptor buildAssetFileDescriptor(ByteArrayOutputStream stream) { 6730 try { 6731 stream.flush(); 6732 6733 final byte[] byteData = stream.toByteArray(); 6734 6735 return makeAssetFileDescriptor( 6736 ParcelFileDescriptor.fromData(byteData, CONTACT_MEMORY_FILE_NAME), 6737 byteData.length); 6738 } catch (IOException e) { 6739 Log.w(TAG, "Problem writing stream into an ParcelFileDescriptor: " + e.toString()); 6740 return null; 6741 } 6742 } 6743 6744 private AssetFileDescriptor makeAssetFileDescriptor(ParcelFileDescriptor fd) { 6745 return makeAssetFileDescriptor(fd, AssetFileDescriptor.UNKNOWN_LENGTH); 6746 } 6747 6748 private AssetFileDescriptor makeAssetFileDescriptor(ParcelFileDescriptor fd, long length) { 6749 return fd != null ? new AssetFileDescriptor(fd, 0, length) : null; 6750 } 6751 6752 /** 6753 * Output {@link RawContacts} matching the requested selection in the vCard 6754 * format to the given {@link OutputStream}. This method returns silently if 6755 * any errors encountered. 6756 */ 6757 private void outputRawContactsAsVCard(Uri uri, OutputStream stream, 6758 String selection, String[] selectionArgs) { 6759 final Context context = this.getContext(); 6760 int vcardconfig = VCardConfig.VCARD_TYPE_DEFAULT; 6761 if(uri.getBooleanQueryParameter( 6762 Contacts.QUERY_PARAMETER_VCARD_NO_PHOTO, false)) { 6763 vcardconfig |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT; 6764 } 6765 final VCardComposer composer = 6766 new VCardComposer(context, vcardconfig, false); 6767 Writer writer = null; 6768 try { 6769 writer = new BufferedWriter(new OutputStreamWriter(stream)); 6770 if (!composer.init(uri, selection, selectionArgs, null)) { 6771 Log.w(TAG, "Failed to init VCardComposer"); 6772 return; 6773 } 6774 6775 while (!composer.isAfterLast()) { 6776 writer.write(composer.createOneEntry()); 6777 } 6778 } catch (IOException e) { 6779 Log.e(TAG, "IOException: " + e); 6780 } finally { 6781 composer.terminate(); 6782 if (writer != null) { 6783 try { 6784 writer.close(); 6785 } catch (IOException e) { 6786 Log.w(TAG, "IOException during closing output stream: " + e); 6787 } 6788 } 6789 } 6790 } 6791 6792 @Override 6793 public String getType(Uri uri) { 6794 6795 waitForAccess(mReadAccessLatch); 6796 6797 final int match = sUriMatcher.match(uri); 6798 switch (match) { 6799 case CONTACTS: 6800 return Contacts.CONTENT_TYPE; 6801 case CONTACTS_LOOKUP: 6802 case CONTACTS_ID: 6803 case CONTACTS_LOOKUP_ID: 6804 case PROFILE: 6805 return Contacts.CONTENT_ITEM_TYPE; 6806 case CONTACTS_AS_VCARD: 6807 case CONTACTS_AS_MULTI_VCARD: 6808 case PROFILE_AS_VCARD: 6809 return Contacts.CONTENT_VCARD_TYPE; 6810 case CONTACTS_ID_PHOTO: 6811 case CONTACTS_ID_DISPLAY_PHOTO: 6812 case CONTACTS_LOOKUP_DISPLAY_PHOTO: 6813 case CONTACTS_LOOKUP_ID_DISPLAY_PHOTO: 6814 case RAW_CONTACTS_ID_DISPLAY_PHOTO: 6815 case DISPLAY_PHOTO: 6816 return "image/jpeg"; 6817 case RAW_CONTACTS: 6818 case PROFILE_RAW_CONTACTS: 6819 return RawContacts.CONTENT_TYPE; 6820 case RAW_CONTACTS_ID: 6821 case PROFILE_RAW_CONTACTS_ID: 6822 return RawContacts.CONTENT_ITEM_TYPE; 6823 case DATA: 6824 case PROFILE_DATA: 6825 return Data.CONTENT_TYPE; 6826 case DATA_ID: 6827 return mDbHelper.getDataMimeType(ContentUris.parseId(uri)); 6828 case PHONES: 6829 return Phone.CONTENT_TYPE; 6830 case PHONES_ID: 6831 return Phone.CONTENT_ITEM_TYPE; 6832 case PHONE_LOOKUP: 6833 return PhoneLookup.CONTENT_TYPE; 6834 case EMAILS: 6835 return Email.CONTENT_TYPE; 6836 case EMAILS_ID: 6837 return Email.CONTENT_ITEM_TYPE; 6838 case POSTALS: 6839 return StructuredPostal.CONTENT_TYPE; 6840 case POSTALS_ID: 6841 return StructuredPostal.CONTENT_ITEM_TYPE; 6842 case AGGREGATION_EXCEPTIONS: 6843 return AggregationExceptions.CONTENT_TYPE; 6844 case AGGREGATION_EXCEPTION_ID: 6845 return AggregationExceptions.CONTENT_ITEM_TYPE; 6846 case SETTINGS: 6847 return Settings.CONTENT_TYPE; 6848 case AGGREGATION_SUGGESTIONS: 6849 return Contacts.CONTENT_TYPE; 6850 case SEARCH_SUGGESTIONS: 6851 return SearchManager.SUGGEST_MIME_TYPE; 6852 case SEARCH_SHORTCUT: 6853 return SearchManager.SHORTCUT_MIME_TYPE; 6854 case DIRECTORIES: 6855 return Directory.CONTENT_TYPE; 6856 case DIRECTORIES_ID: 6857 return Directory.CONTENT_ITEM_TYPE; 6858 default: 6859 return mLegacyApiSupport.getType(uri); 6860 } 6861 } 6862 6863 public String[] getDefaultProjection(Uri uri) { 6864 final int match = sUriMatcher.match(uri); 6865 switch (match) { 6866 case CONTACTS: 6867 case CONTACTS_LOOKUP: 6868 case CONTACTS_ID: 6869 case CONTACTS_LOOKUP_ID: 6870 case AGGREGATION_SUGGESTIONS: 6871 case PROFILE: 6872 return sContactsProjectionMap.getColumnNames(); 6873 6874 case CONTACTS_ID_ENTITIES: 6875 case PROFILE_ENTITIES: 6876 return sEntityProjectionMap.getColumnNames(); 6877 6878 case CONTACTS_AS_VCARD: 6879 case CONTACTS_AS_MULTI_VCARD: 6880 case PROFILE_AS_VCARD: 6881 return sContactsVCardProjectionMap.getColumnNames(); 6882 6883 case RAW_CONTACTS: 6884 case RAW_CONTACTS_ID: 6885 case PROFILE_RAW_CONTACTS: 6886 case PROFILE_RAW_CONTACTS_ID: 6887 return sRawContactsProjectionMap.getColumnNames(); 6888 6889 case DATA_ID: 6890 case PHONES: 6891 case PHONES_ID: 6892 case EMAILS: 6893 case EMAILS_ID: 6894 case POSTALS: 6895 case POSTALS_ID: 6896 case PROFILE_DATA: 6897 return sDataProjectionMap.getColumnNames(); 6898 6899 case PHONE_LOOKUP: 6900 return sPhoneLookupProjectionMap.getColumnNames(); 6901 6902 case AGGREGATION_EXCEPTIONS: 6903 case AGGREGATION_EXCEPTION_ID: 6904 return sAggregationExceptionsProjectionMap.getColumnNames(); 6905 6906 case SETTINGS: 6907 return sSettingsProjectionMap.getColumnNames(); 6908 6909 case DIRECTORIES: 6910 case DIRECTORIES_ID: 6911 return sDirectoryProjectionMap.getColumnNames(); 6912 6913 default: 6914 return null; 6915 } 6916 } 6917 6918 private class StructuredNameLookupBuilder extends NameLookupBuilder { 6919 6920 public StructuredNameLookupBuilder(NameSplitter splitter) { 6921 super(splitter); 6922 } 6923 6924 @Override 6925 protected void insertNameLookup(long rawContactId, long dataId, int lookupType, 6926 String name) { 6927 mDbHelper.insertNameLookup(rawContactId, dataId, lookupType, name); 6928 } 6929 6930 @Override 6931 protected String[] getCommonNicknameClusters(String normalizedName) { 6932 return mCommonNicknameCache.getCommonNicknameClusters(normalizedName); 6933 } 6934 } 6935 6936 public void appendContactFilterAsNestedQuery(StringBuilder sb, String filterParam) { 6937 sb.append("(" + 6938 "SELECT DISTINCT " + RawContacts.CONTACT_ID + 6939 " FROM " + Tables.RAW_CONTACTS + 6940 " JOIN " + Tables.NAME_LOOKUP + 6941 " ON(" + RawContactsColumns.CONCRETE_ID + "=" 6942 + NameLookupColumns.RAW_CONTACT_ID + ")" + 6943 " WHERE normalized_name GLOB '"); 6944 sb.append(NameNormalizer.normalize(filterParam)); 6945 sb.append("*' AND " + NameLookupColumns.NAME_TYPE + 6946 " IN(" + CONTACT_LOOKUP_NAME_TYPES + "))"); 6947 } 6948 6949 public boolean isPhoneNumber(String filter) { 6950 boolean atLeastOneDigit = false; 6951 int len = filter.length(); 6952 for (int i = 0; i < len; i++) { 6953 char c = filter.charAt(i); 6954 if (c >= '0' && c <= '9') { 6955 atLeastOneDigit = true; 6956 } else if (c != '*' && c != '#' && c != '+' && c != 'N' && c != '.' && c != ';' 6957 && c != '-' && c != '(' && c != ')' && c != ' ') { 6958 return false; 6959 } 6960 } 6961 return atLeastOneDigit; 6962 } 6963 6964 /** 6965 * Takes components of a name from the query parameters and returns a cursor with those 6966 * components as well as all missing components. There is no database activity involved 6967 * in this so the call can be made on the UI thread. 6968 */ 6969 private Cursor completeName(Uri uri, String[] projection) { 6970 if (projection == null) { 6971 projection = sDataProjectionMap.getColumnNames(); 6972 } 6973 6974 ContentValues values = new ContentValues(); 6975 DataRowHandlerForStructuredName handler = (DataRowHandlerForStructuredName) 6976 getDataRowHandler(StructuredName.CONTENT_ITEM_TYPE); 6977 6978 copyQueryParamsToContentValues(values, uri, 6979 StructuredName.DISPLAY_NAME, 6980 StructuredName.PREFIX, 6981 StructuredName.GIVEN_NAME, 6982 StructuredName.MIDDLE_NAME, 6983 StructuredName.FAMILY_NAME, 6984 StructuredName.SUFFIX, 6985 StructuredName.PHONETIC_NAME, 6986 StructuredName.PHONETIC_FAMILY_NAME, 6987 StructuredName.PHONETIC_MIDDLE_NAME, 6988 StructuredName.PHONETIC_GIVEN_NAME 6989 ); 6990 6991 handler.fixStructuredNameComponents(values, values); 6992 6993 MatrixCursor cursor = new MatrixCursor(projection); 6994 Object[] row = new Object[projection.length]; 6995 for (int i = 0; i < projection.length; i++) { 6996 row[i] = values.get(projection[i]); 6997 } 6998 cursor.addRow(row); 6999 return cursor; 7000 } 7001 7002 private void copyQueryParamsToContentValues(ContentValues values, Uri uri, String... columns) { 7003 for (String column : columns) { 7004 String param = uri.getQueryParameter(column); 7005 if (param != null) { 7006 values.put(column, param); 7007 } 7008 } 7009 } 7010 7011 7012 /** 7013 * Inserts an argument at the beginning of the selection arg list. 7014 */ 7015 private String[] insertSelectionArg(String[] selectionArgs, String arg) { 7016 if (selectionArgs == null) { 7017 return new String[] {arg}; 7018 } else { 7019 int newLength = selectionArgs.length + 1; 7020 String[] newSelectionArgs = new String[newLength]; 7021 newSelectionArgs[0] = arg; 7022 System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length); 7023 return newSelectionArgs; 7024 } 7025 } 7026 7027 private String[] appendProjectionArg(String[] projection, String arg) { 7028 if (projection == null) { 7029 return null; 7030 } 7031 final int length = projection.length; 7032 String[] newProjection = new String[length + 1]; 7033 System.arraycopy(projection, 0, newProjection, 0, length); 7034 newProjection[length] = arg; 7035 return newProjection; 7036 } 7037 7038 protected Account getDefaultAccount() { 7039 AccountManager accountManager = AccountManager.get(getContext()); 7040 try { 7041 Account[] accounts = accountManager.getAccountsByType(DEFAULT_ACCOUNT_TYPE); 7042 if (accounts != null && accounts.length > 0) { 7043 return accounts[0]; 7044 } 7045 } catch (Throwable e) { 7046 Log.e(TAG, "Cannot determine the default account for contacts compatibility", e); 7047 } 7048 return null; 7049 } 7050 7051 /** 7052 * Returns true if the specified account type and data set is writable. 7053 */ 7054 protected boolean isWritableAccountWithDataSet(String accountTypeAndDataSet) { 7055 if (accountTypeAndDataSet == null) { 7056 return true; 7057 } 7058 7059 Boolean writable = mAccountWritability.get(accountTypeAndDataSet); 7060 if (writable != null) { 7061 return writable; 7062 } 7063 7064 IContentService contentService = ContentResolver.getContentService(); 7065 try { 7066 // TODO(dsantoro): Need to update this logic to allow for sub-accounts. 7067 for (SyncAdapterType sync : contentService.getSyncAdapterTypes()) { 7068 if (ContactsContract.AUTHORITY.equals(sync.authority) && 7069 accountTypeAndDataSet.equals(sync.accountType)) { 7070 writable = sync.supportsUploading(); 7071 break; 7072 } 7073 } 7074 } catch (RemoteException e) { 7075 Log.e(TAG, "Could not acquire sync adapter types"); 7076 } 7077 7078 if (writable == null) { 7079 writable = false; 7080 } 7081 7082 mAccountWritability.put(accountTypeAndDataSet, writable); 7083 return writable; 7084 } 7085 7086 7087 /* package */ static boolean readBooleanQueryParameter(Uri uri, String parameter, 7088 boolean defaultValue) { 7089 7090 // Manually parse the query, which is much faster than calling uri.getQueryParameter 7091 String query = uri.getEncodedQuery(); 7092 if (query == null) { 7093 return defaultValue; 7094 } 7095 7096 int index = query.indexOf(parameter); 7097 if (index == -1) { 7098 return defaultValue; 7099 } 7100 7101 index += parameter.length(); 7102 7103 return !matchQueryParameter(query, index, "=0", false) 7104 && !matchQueryParameter(query, index, "=false", true); 7105 } 7106 7107 private static boolean matchQueryParameter(String query, int index, String value, 7108 boolean ignoreCase) { 7109 int length = value.length(); 7110 return query.regionMatches(ignoreCase, index, value, 0, length) 7111 && (query.length() == index + length || query.charAt(index + length) == '&'); 7112 } 7113 7114 /** 7115 * A fast re-implementation of {@link Uri#getQueryParameter} 7116 */ 7117 /* package */ static String getQueryParameter(Uri uri, String parameter) { 7118 String query = uri.getEncodedQuery(); 7119 if (query == null) { 7120 return null; 7121 } 7122 7123 int queryLength = query.length(); 7124 int parameterLength = parameter.length(); 7125 7126 String value; 7127 int index = 0; 7128 while (true) { 7129 index = query.indexOf(parameter, index); 7130 if (index == -1) { 7131 return null; 7132 } 7133 7134 // Should match against the whole parameter instead of its suffix. 7135 // e.g. The parameter "param" must not be found in "some_param=val". 7136 if (index > 0) { 7137 char prevChar = query.charAt(index - 1); 7138 if (prevChar != '?' && prevChar != '&') { 7139 // With "some_param=val1¶m=val2", we should find second "param" occurrence. 7140 index += parameterLength; 7141 continue; 7142 } 7143 } 7144 7145 index += parameterLength; 7146 7147 if (queryLength == index) { 7148 return null; 7149 } 7150 7151 if (query.charAt(index) == '=') { 7152 index++; 7153 break; 7154 } 7155 } 7156 7157 int ampIndex = query.indexOf('&', index); 7158 if (ampIndex == -1) { 7159 value = query.substring(index); 7160 } else { 7161 value = query.substring(index, ampIndex); 7162 } 7163 7164 return Uri.decode(value); 7165 } 7166 7167 protected boolean isAggregationUpgradeNeeded() { 7168 if (!mContactAggregator.isEnabled()) { 7169 return false; 7170 } 7171 7172 int version = Integer.parseInt(mDbHelper.getProperty(PROPERTY_AGGREGATION_ALGORITHM, "1")); 7173 return version < PROPERTY_AGGREGATION_ALGORITHM_VERSION; 7174 } 7175 7176 protected void upgradeAggregationAlgorithmInBackground() { 7177 // This upgrade will affect very few contacts, so it can be performed on the 7178 // main thread during the initial boot after an OTA 7179 7180 Log.i(TAG, "Upgrading aggregation algorithm"); 7181 int count = 0; 7182 long start = SystemClock.currentThreadTimeMillis(); 7183 try { 7184 mDb = mDbHelper.getWritableDatabase(); 7185 mDb.beginTransaction(); 7186 Cursor cursor = mDb.query(true, 7187 Tables.RAW_CONTACTS + " r1 JOIN " + Tables.RAW_CONTACTS + " r2", 7188 new String[]{"r1." + RawContacts._ID}, 7189 "r1." + RawContacts._ID + "!=r2." + RawContacts._ID + 7190 " AND r1." + RawContacts.CONTACT_ID + "=r2." + RawContacts.CONTACT_ID + 7191 " AND r1." + RawContacts.ACCOUNT_NAME + "=r2." + RawContacts.ACCOUNT_NAME + 7192 " AND r1." + RawContacts.ACCOUNT_TYPE + "=r2." + RawContacts.ACCOUNT_TYPE + 7193 " AND r1." + RawContacts.DATA_SET + "=r2." + RawContacts.DATA_SET, 7194 null, null, null, null, null); 7195 try { 7196 while (cursor.moveToNext()) { 7197 long rawContactId = cursor.getLong(0); 7198 mContactAggregator.markForAggregation(rawContactId, 7199 RawContacts.AGGREGATION_MODE_DEFAULT, true); 7200 count++; 7201 } 7202 } finally { 7203 cursor.close(); 7204 } 7205 mContactAggregator.aggregateInTransaction(mTransactionContext, mDb); 7206 updateSearchIndexInTransaction(); 7207 mDb.setTransactionSuccessful(); 7208 mDbHelper.setProperty(PROPERTY_AGGREGATION_ALGORITHM, 7209 String.valueOf(PROPERTY_AGGREGATION_ALGORITHM_VERSION)); 7210 } finally { 7211 mDb.endTransaction(); 7212 long end = SystemClock.currentThreadTimeMillis(); 7213 Log.i(TAG, "Aggregation algorithm upgraded for " + count 7214 + " contacts, in " + (end - start) + "ms"); 7215 } 7216 } 7217 7218 /* Visible for testing */ 7219 boolean isPhone() { 7220 if (!sIsPhoneInitialized) { 7221 sIsPhone = new TelephonyManager(getContext()).isVoiceCapable(); 7222 sIsPhoneInitialized = true; 7223 } 7224 return sIsPhone; 7225 } 7226 7227 private boolean handleDataUsageFeedback(Uri uri) { 7228 final long currentTimeMillis = System.currentTimeMillis(); 7229 final String usageType = uri.getQueryParameter(DataUsageFeedback.USAGE_TYPE); 7230 final String[] ids = uri.getLastPathSegment().trim().split(","); 7231 final ArrayList<Long> dataIds = new ArrayList<Long>(); 7232 7233 for (String id : ids) { 7234 dataIds.add(Long.valueOf(id)); 7235 } 7236 final boolean successful; 7237 if (TextUtils.isEmpty(usageType)) { 7238 Log.w(TAG, "Method for data usage feedback isn't specified. Ignoring."); 7239 successful = false; 7240 } else { 7241 successful = updateDataUsageStat(dataIds, usageType, currentTimeMillis) > 0; 7242 } 7243 7244 // Handle old API. This doesn't affect the result of this entire method. 7245 final String[] questionMarks = new String[ids.length]; 7246 Arrays.fill(questionMarks, "?"); 7247 final String where = Data._ID + " IN (" + TextUtils.join(",", questionMarks) + ")"; 7248 final Cursor cursor = mDb.query( 7249 Views.DATA, 7250 new String[] { Data.CONTACT_ID }, 7251 where, ids, null, null, null); 7252 try { 7253 while (cursor.moveToNext()) { 7254 mSelectionArgs1[0] = cursor.getString(0); 7255 ContentValues values2 = new ContentValues(); 7256 values2.put(Contacts.LAST_TIME_CONTACTED, currentTimeMillis); 7257 mDb.update(Tables.CONTACTS, values2, Contacts._ID + "=?", mSelectionArgs1); 7258 mDb.execSQL(UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1); 7259 mDb.execSQL(UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1); 7260 } 7261 } finally { 7262 cursor.close(); 7263 } 7264 7265 return successful; 7266 } 7267 7268 /** 7269 * Update {@link Tables#DATA_USAGE_STAT}. 7270 * 7271 * @return the number of rows affected. 7272 */ 7273 @VisibleForTesting 7274 /* package */ int updateDataUsageStat( 7275 List<Long> dataIds, String type, long currentTimeMillis) { 7276 final int typeInt = sDataUsageTypeMap.get(type); 7277 final String where = DataUsageStatColumns.DATA_ID + " =? AND " 7278 + DataUsageStatColumns.USAGE_TYPE_INT + " =?"; 7279 final String[] columns = 7280 new String[] { DataUsageStatColumns._ID, DataUsageStatColumns.TIMES_USED }; 7281 final ContentValues values = new ContentValues(); 7282 for (Long dataId : dataIds) { 7283 final String[] args = new String[] { dataId.toString(), String.valueOf(typeInt) }; 7284 mDb.beginTransaction(); 7285 try { 7286 final Cursor cursor = mDb.query(Tables.DATA_USAGE_STAT, columns, where, args, 7287 null, null, null); 7288 try { 7289 if (cursor.getCount() > 0) { 7290 if (!cursor.moveToFirst()) { 7291 Log.e(TAG, 7292 "moveToFirst() failed while getAccount() returned non-zero."); 7293 } else { 7294 values.clear(); 7295 values.put(DataUsageStatColumns.TIMES_USED, cursor.getInt(1) + 1); 7296 values.put(DataUsageStatColumns.LAST_TIME_USED, currentTimeMillis); 7297 mDb.update(Tables.DATA_USAGE_STAT, values, 7298 DataUsageStatColumns._ID + " =?", 7299 new String[] { cursor.getString(0) }); 7300 } 7301 } else { 7302 values.clear(); 7303 values.put(DataUsageStatColumns.DATA_ID, dataId); 7304 values.put(DataUsageStatColumns.USAGE_TYPE_INT, typeInt); 7305 values.put(DataUsageStatColumns.TIMES_USED, 1); 7306 values.put(DataUsageStatColumns.LAST_TIME_USED, currentTimeMillis); 7307 mDb.insert(Tables.DATA_USAGE_STAT, null, values); 7308 } 7309 mDb.setTransactionSuccessful(); 7310 } finally { 7311 cursor.close(); 7312 } 7313 } finally { 7314 mDb.endTransaction(); 7315 } 7316 } 7317 7318 return dataIds.size(); 7319 } 7320 7321 /** 7322 * Returns a sort order String for promoting data rows (email addresses, phone numbers, etc.) 7323 * associated with a primary account. The primary account should be supplied from applications 7324 * with {@link ContactsContract#PRIMARY_ACCOUNT_NAME} and 7325 * {@link ContactsContract#PRIMARY_ACCOUNT_TYPE}. Null will be returned when the primary 7326 * account isn't available. 7327 */ 7328 private String getAccountPromotionSortOrder(Uri uri) { 7329 final String primaryAccountName = 7330 uri.getQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME); 7331 final String primaryAccountType = 7332 uri.getQueryParameter(ContactsContract.PRIMARY_ACCOUNT_TYPE); 7333 7334 // Data rows associated with primary account should be promoted. 7335 if (!TextUtils.isEmpty(primaryAccountName)) { 7336 StringBuilder sb = new StringBuilder(); 7337 sb.append("(CASE WHEN " + RawContacts.ACCOUNT_NAME + "="); 7338 DatabaseUtils.appendEscapedSQLString(sb, primaryAccountName); 7339 if (!TextUtils.isEmpty(primaryAccountType)) { 7340 sb.append(" AND " + RawContacts.ACCOUNT_TYPE + "="); 7341 DatabaseUtils.appendEscapedSQLString(sb, primaryAccountType); 7342 } 7343 sb.append(" THEN 0 ELSE 1 END)"); 7344 return sb.toString(); 7345 } else { 7346 return null; 7347 } 7348 } 7349} 7350