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&param=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