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.net.Uri; 25 import android.provider.ContactsContract; 26 import android.provider.ContactsContract.Contacts; 27 import android.text.TextUtils; 28 import android.util.Slog; 29 30 /** A helper class that queries the Contacts database. */ 31 class ContactsQueryHelper { 32 33 private static final String TAG = "ContactsQueryHelper"; 34 35 private final Context mContext; 36 private Uri mContactUri; 37 private boolean mIsStarred; 38 private String mPhoneNumber; 39 private long mLastUpdatedTimestamp; 40 ContactsQueryHelper(Context context)41 ContactsQueryHelper(Context context) { 42 mContext = context; 43 } 44 45 /** 46 * Queries the Contacts database with the given contact URI and returns whether the query runs 47 * successfully. 48 */ 49 @WorkerThread query(@onNull String contactUri)50 boolean query(@NonNull String contactUri) { 51 if (TextUtils.isEmpty(contactUri)) { 52 return false; 53 } 54 Uri uri = Uri.parse(contactUri); 55 if ("tel".equals(uri.getScheme())) { 56 return queryWithPhoneNumber(uri.getSchemeSpecificPart()); 57 } else if ("mailto".equals(uri.getScheme())) { 58 return queryWithEmail(uri.getSchemeSpecificPart()); 59 } else if (contactUri.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) { 60 return queryWithUri(uri); 61 } 62 return false; 63 } 64 65 /** Queries the Contacts database and read the most recently updated contact. */ 66 @WorkerThread querySince(long sinceTime)67 boolean querySince(long sinceTime) { 68 final String[] projection = new String[] { 69 Contacts._ID, Contacts.LOOKUP_KEY, Contacts.STARRED, Contacts.HAS_PHONE_NUMBER, 70 Contacts.CONTACT_LAST_UPDATED_TIMESTAMP }; 71 String selection = Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + " > ?"; 72 String[] selectionArgs = new String[] {Long.toString(sinceTime)}; 73 return queryContact(Contacts.CONTENT_URI, projection, selection, selectionArgs); 74 } 75 76 @Nullable getContactUri()77 Uri getContactUri() { 78 return mContactUri; 79 } 80 isStarred()81 boolean isStarred() { 82 return mIsStarred; 83 } 84 85 @Nullable getPhoneNumber()86 String getPhoneNumber() { 87 return mPhoneNumber; 88 } 89 getLastUpdatedTimestamp()90 long getLastUpdatedTimestamp() { 91 return mLastUpdatedTimestamp; 92 } 93 queryWithPhoneNumber(String phoneNumber)94 private boolean queryWithPhoneNumber(String phoneNumber) { 95 Uri phoneUri = Uri.withAppendedPath( 96 ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber)); 97 return queryWithUri(phoneUri); 98 } 99 queryWithEmail(String email)100 private boolean queryWithEmail(String email) { 101 Uri emailUri = Uri.withAppendedPath( 102 ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI, Uri.encode(email)); 103 return queryWithUri(emailUri); 104 } 105 queryWithUri(@onNull Uri uri)106 private boolean queryWithUri(@NonNull Uri uri) { 107 final String[] projection = new String[] { 108 Contacts._ID, Contacts.LOOKUP_KEY, Contacts.STARRED, Contacts.HAS_PHONE_NUMBER }; 109 return queryContact(uri, projection, /* selection= */ null, /* selectionArgs= */ null); 110 } 111 queryContact(@onNull Uri uri, @NonNull String[] projection, @Nullable String selection, @Nullable String[] selectionArgs)112 private boolean queryContact(@NonNull Uri uri, @NonNull String[] projection, 113 @Nullable String selection, @Nullable String[] selectionArgs) { 114 long contactId; 115 String lookupKey = null; 116 boolean hasPhoneNumber = false; 117 boolean found = false; 118 try (Cursor cursor = mContext.getContentResolver().query( 119 uri, projection, selection, selectionArgs, /* sortOrder= */ null)) { 120 if (cursor == null) { 121 Slog.w(TAG, "Cursor is null when querying contact."); 122 return false; 123 } 124 while (cursor.moveToNext()) { 125 // Contact ID 126 int idIndex = cursor.getColumnIndex(Contacts._ID); 127 contactId = cursor.getLong(idIndex); 128 129 // Lookup key 130 int lookupKeyIndex = cursor.getColumnIndex(Contacts.LOOKUP_KEY); 131 lookupKey = cursor.getString(lookupKeyIndex); 132 133 mContactUri = Contacts.getLookupUri(contactId, lookupKey); 134 135 // Starred 136 int starredIndex = cursor.getColumnIndex(Contacts.STARRED); 137 mIsStarred = cursor.getInt(starredIndex) != 0; 138 139 // Has phone number 140 int hasPhoneNumIndex = cursor.getColumnIndex(Contacts.HAS_PHONE_NUMBER); 141 hasPhoneNumber = cursor.getInt(hasPhoneNumIndex) != 0; 142 143 // Last updated timestamp 144 int lastUpdatedTimestampIndex = cursor.getColumnIndex( 145 Contacts.CONTACT_LAST_UPDATED_TIMESTAMP); 146 if (lastUpdatedTimestampIndex >= 0) { 147 mLastUpdatedTimestamp = cursor.getLong(lastUpdatedTimestampIndex); 148 } 149 150 found = true; 151 } 152 } 153 if (found && lookupKey != null && hasPhoneNumber) { 154 return queryPhoneNumber(lookupKey); 155 } 156 return found; 157 } 158 queryPhoneNumber(String lookupKey)159 private boolean queryPhoneNumber(String lookupKey) { 160 String[] projection = new String[] { 161 ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER }; 162 String selection = Contacts.LOOKUP_KEY + " = ?"; 163 String[] selectionArgs = new String[] { lookupKey }; 164 try (Cursor cursor = mContext.getContentResolver().query( 165 ContactsContract.CommonDataKinds.Phone.CONTENT_URI, projection, selection, 166 selectionArgs, /* sortOrder= */ null)) { 167 if (cursor == null) { 168 Slog.w(TAG, "Cursor is null when querying contact phone number."); 169 return false; 170 } 171 while (cursor.moveToNext()) { 172 // Phone number 173 int phoneNumIdx = cursor.getColumnIndex( 174 ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER); 175 if (phoneNumIdx >= 0) { 176 mPhoneNumber = cursor.getString(phoneNumIdx); 177 } 178 } 179 } 180 return true; 181 } 182 } 183