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