1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.people.data; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.annotation.WorkerThread; 22 import android.content.Context; 23 import android.database.Cursor; 24 import android.database.sqlite.SQLiteException; 25 import android.net.Uri; 26 import android.provider.ContactsContract; 27 import android.provider.ContactsContract.Contacts; 28 import android.text.TextUtils; 29 import android.util.Slog; 30 31 /** A helper class that queries the Contacts database. */ 32 class ContactsQueryHelper { 33 34 private static final String TAG = "ContactsQueryHelper"; 35 36 private final Context mContext; 37 private Uri mContactUri; 38 private boolean mIsStarred; 39 private String mPhoneNumber; 40 private long mLastUpdatedTimestamp; 41 ContactsQueryHelper(Context context)42 ContactsQueryHelper(Context context) { 43 mContext = context; 44 } 45 46 /** 47 * Queries the Contacts database with the given contact URI and returns whether the query runs 48 * successfully. 49 */ 50 @WorkerThread query(@onNull String contactUri)51 boolean query(@NonNull String contactUri) { 52 if (TextUtils.isEmpty(contactUri)) { 53 return false; 54 } 55 Uri uri = Uri.parse(contactUri); 56 if ("tel".equals(uri.getScheme())) { 57 return queryWithPhoneNumber(uri.getSchemeSpecificPart()); 58 } else if ("mailto".equals(uri.getScheme())) { 59 return queryWithEmail(uri.getSchemeSpecificPart()); 60 } else if (contactUri.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) { 61 return queryWithUri(uri); 62 } 63 return false; 64 } 65 66 /** Queries the Contacts database and read the most recently updated contact. */ 67 @WorkerThread querySince(long sinceTime)68 boolean querySince(long sinceTime) { 69 final String[] projection = new String[] { 70 Contacts._ID, Contacts.LOOKUP_KEY, Contacts.STARRED, Contacts.HAS_PHONE_NUMBER, 71 Contacts.CONTACT_LAST_UPDATED_TIMESTAMP }; 72 String selection = Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + " > ?"; 73 String[] selectionArgs = new String[] {Long.toString(sinceTime)}; 74 return queryContact(Contacts.CONTENT_URI, projection, selection, selectionArgs); 75 } 76 77 @Nullable getContactUri()78 Uri getContactUri() { 79 return mContactUri; 80 } 81 isStarred()82 boolean isStarred() { 83 return mIsStarred; 84 } 85 86 @Nullable getPhoneNumber()87 String getPhoneNumber() { 88 return mPhoneNumber; 89 } 90 getLastUpdatedTimestamp()91 long getLastUpdatedTimestamp() { 92 return mLastUpdatedTimestamp; 93 } 94 queryWithPhoneNumber(String phoneNumber)95 private boolean queryWithPhoneNumber(String phoneNumber) { 96 Uri phoneUri = Uri.withAppendedPath( 97 ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber)); 98 return queryWithUri(phoneUri); 99 } 100 queryWithEmail(String email)101 private boolean queryWithEmail(String email) { 102 Uri emailUri = Uri.withAppendedPath( 103 ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI, Uri.encode(email)); 104 return queryWithUri(emailUri); 105 } 106 queryWithUri(@onNull Uri uri)107 private boolean queryWithUri(@NonNull Uri uri) { 108 final String[] projection = new String[] { 109 Contacts._ID, Contacts.LOOKUP_KEY, Contacts.STARRED, Contacts.HAS_PHONE_NUMBER }; 110 return queryContact(uri, projection, /* selection= */ null, /* selectionArgs= */ null); 111 } 112 queryContact(@onNull Uri uri, @NonNull String[] projection, @Nullable String selection, @Nullable String[] selectionArgs)113 private boolean queryContact(@NonNull Uri uri, @NonNull String[] projection, 114 @Nullable String selection, @Nullable String[] selectionArgs) { 115 long contactId; 116 String lookupKey = null; 117 boolean hasPhoneNumber = false; 118 boolean found = false; 119 try (Cursor cursor = mContext.getContentResolver().query( 120 uri, projection, selection, selectionArgs, /* sortOrder= */ null)) { 121 if (cursor == null) { 122 Slog.w(TAG, "Cursor is null when querying contact."); 123 return false; 124 } 125 while (cursor.moveToNext()) { 126 // Contact ID 127 int idIndex = cursor.getColumnIndex(Contacts._ID); 128 contactId = cursor.getLong(idIndex); 129 130 // Lookup key 131 int lookupKeyIndex = cursor.getColumnIndex(Contacts.LOOKUP_KEY); 132 lookupKey = cursor.getString(lookupKeyIndex); 133 134 mContactUri = Contacts.getLookupUri(contactId, lookupKey); 135 136 // Starred 137 int starredIndex = cursor.getColumnIndex(Contacts.STARRED); 138 mIsStarred = cursor.getInt(starredIndex) != 0; 139 140 // Has phone number 141 int hasPhoneNumIndex = cursor.getColumnIndex(Contacts.HAS_PHONE_NUMBER); 142 hasPhoneNumber = cursor.getInt(hasPhoneNumIndex) != 0; 143 144 // Last updated timestamp 145 int lastUpdatedTimestampIndex = cursor.getColumnIndex( 146 Contacts.CONTACT_LAST_UPDATED_TIMESTAMP); 147 if (lastUpdatedTimestampIndex >= 0) { 148 mLastUpdatedTimestamp = cursor.getLong(lastUpdatedTimestampIndex); 149 } 150 151 found = true; 152 } 153 } catch (SQLiteException exception) { 154 Slog.w("SQLite exception when querying contacts.", exception); 155 } catch (IllegalArgumentException exception) { 156 Slog.w("Illegal Argument exception when querying contacts.", exception); 157 } 158 if (found && lookupKey != null && hasPhoneNumber) { 159 return queryPhoneNumber(lookupKey); 160 } 161 return found; 162 } 163 queryPhoneNumber(String lookupKey)164 private boolean queryPhoneNumber(String lookupKey) { 165 String[] projection = new String[] { 166 ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER }; 167 String selection = Contacts.LOOKUP_KEY + " = ?"; 168 String[] selectionArgs = new String[] { lookupKey }; 169 try (Cursor cursor = mContext.getContentResolver().query( 170 ContactsContract.CommonDataKinds.Phone.CONTENT_URI, projection, selection, 171 selectionArgs, /* sortOrder= */ null)) { 172 if (cursor == null) { 173 Slog.w(TAG, "Cursor is null when querying contact phone number."); 174 return false; 175 } 176 while (cursor.moveToNext()) { 177 // Phone number 178 int phoneNumIdx = cursor.getColumnIndex( 179 ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER); 180 if (phoneNumIdx >= 0) { 181 mPhoneNumber = cursor.getString(phoneNumIdx); 182 } 183 } 184 } 185 return true; 186 } 187 } 188