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