• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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.appsearch.contactsindexer;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.app.appsearch.util.LogUtil;
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.provider.ContactsContract.DeletedContacts;
29 import android.util.Log;
30 
31 import java.util.List;
32 import java.util.Objects;
33 
34 /**
35  * Helper class to query Contacts Provider (CP2).
36  *
37  * @hide
38  */
39 public final class ContactsProviderUtil {
40     private static final String TAG = "ContactsProviderHelper";
41 
42     public static final int UPDATE_LIMIT_NONE = -1;
43 
44     // static final string for querying CP2
45     private static final String UPDATE_SINCE = Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + ">?";
46     private static final String UPDATE_ORDER_BY = Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + " DESC";
47     private static final String[] UPDATE_SELECTION =
48             new String[] {Contacts._ID, Contacts.CONTACT_LAST_UPDATED_TIMESTAMP};
49     private static final String DELETION_SINCE = DeletedContacts.CONTACT_DELETED_TIMESTAMP + ">?";
50     private static final String[] DELETION_SELECTION =
51             new String[] {
52                 DeletedContacts.CONTACT_ID, DeletedContacts.CONTACT_DELETED_TIMESTAMP,
53             };
54 
ContactsProviderUtil()55     private ContactsProviderUtil() {}
56 
getLastUpdatedTimestamp(@onNull Cursor cursor)57     static long getLastUpdatedTimestamp(@NonNull Cursor cursor) {
58         Objects.requireNonNull(cursor);
59         int index = cursor.getColumnIndex(Contacts.CONTACT_LAST_UPDATED_TIMESTAMP);
60         return index != -1 ? cursor.getLong(index) : 0;
61     }
62 
63     /**
64      * Gets the ids for deleted contacts from certain timestamp.
65      *
66      * @param sinceFilter timestamp (milliseconds since epoch) from which ids of deleted contacts
67      *     should be returned.
68      * @param contactIds the Set passed in to hold the deleted contacts.
69      * @return the timestamp for the contact most recently deleted.
70      */
getDeletedContactIds( @onNull Context context, long sinceFilter, @NonNull List<String> contactIds, @Nullable ContactsUpdateStats updateStats)71     public static long getDeletedContactIds(
72             @NonNull Context context,
73             long sinceFilter,
74             @NonNull List<String> contactIds,
75             @Nullable ContactsUpdateStats updateStats) {
76         Objects.requireNonNull(context);
77         Objects.requireNonNull(contactIds);
78 
79         String[] selectionArgs = new String[] {Long.toString(sinceFilter)};
80         long newTimestamp = sinceFilter;
81         Cursor cursor = null;
82         try {
83             // TODO(b/203605504) We could optimize the query by setting the sortOrder:
84             //  LAST_DELETED_TIMESTAMP DESC. This way the 1st contact would have the last deleted
85             //  timestamp.
86             cursor =
87                     context.getContentResolver()
88                             .query(
89                                     DeletedContacts.CONTENT_URI,
90                                     DELETION_SELECTION,
91                                     DELETION_SINCE,
92                                     selectionArgs,
93                                     /* sortOrder= */ null);
94 
95             if (cursor == null) {
96                 Log.e(TAG, "Could not fetch deleted contacts - no contacts provider present?");
97                 if (updateStats != null) {
98                     updateStats.mDeleteStatuses.add(ContactsUpdateStats.ERROR_CODE_CP2_NULL_CURSOR);
99                 }
100                 return newTimestamp;
101             }
102 
103             int contactIdIndex = cursor.getColumnIndex(DeletedContacts.CONTACT_ID);
104             int timestampIndex = cursor.getColumnIndex(DeletedContacts.CONTACT_DELETED_TIMESTAMP);
105             long rows = 0;
106             while (cursor.moveToNext()) {
107                 contactIds.add(String.valueOf(cursor.getLong(contactIdIndex)));
108                 // We still get max value between those two here just in case cursor.getLong
109                 // returns something unexpected(e.g. somehow it returns an invalid value like
110                 // -1 or 0 due to an invalid index).
111                 newTimestamp = Math.max(newTimestamp, cursor.getLong(timestampIndex));
112                 ++rows;
113             }
114             if (LogUtil.DEBUG) {
115                 Log.d(TAG, "Got " + rows + " deleted contacts since " + sinceFilter);
116             }
117         } catch (SecurityException
118                 | SQLiteException
119                 | NullPointerException
120                 | NoClassDefFoundError e) {
121             Log.e(TAG, "ContentResolver.query failed to get latest deleted contacts.", e);
122             if (updateStats != null) {
123                 updateStats.mDeleteStatuses.add(
124                         ContactsUpdateStats.ERROR_CODE_CP2_RUNTIME_EXCEPTION);
125             }
126         } finally {
127             if (cursor != null) {
128                 cursor.close();
129             }
130         }
131 
132         return newTimestamp;
133     }
134 
135     /**
136      * Returns a list of IDs, within given limit, of contacts updated since given timestamp.
137      *
138      * @param sinceFilter timestamp (milliseconds since epoch) from which ids of recently updated
139      *     contacts should be returned.
140      * @param contactIds the Set passed in to hold the recently updated contacts.
141      * @param limit the maximum number of contacts fetched from CP2. No limit will be set if the
142      *     value is {@link ContactsIndexerConfig#UPDATE_LIMIT_NONE}.
143      * @return the timestamp for the contact most recently updated.
144      */
getUpdatedContactIds( @onNull Context context, long sinceFilter, int limit, @NonNull List<String> contactIds, @Nullable ContactsUpdateStats updateStats)145     public static long getUpdatedContactIds(
146             @NonNull Context context,
147             long sinceFilter,
148             int limit,
149             @NonNull List<String> contactIds,
150             @Nullable ContactsUpdateStats updateStats) {
151         Objects.requireNonNull(context);
152         Objects.requireNonNull(contactIds);
153 
154         long newTimestamp = sinceFilter;
155         String[] selectionArgs = new String[] {Long.toString(sinceFilter)};
156         // We only get the contacts from the default directory, e.g. the non-invisibles.
157         Uri.Builder contactsUriBuilder =
158                 Contacts.CONTENT_URI
159                         .buildUpon()
160                         .appendQueryParameter(
161                                 ContactsContract.DIRECTORY_PARAM_KEY,
162                                 String.valueOf(ContactsContract.Directory.DEFAULT));
163         String orderBy = null;
164         if (limit >= 0) {
165             contactsUriBuilder.appendQueryParameter(
166                     ContactsContract.LIMIT_PARAM_KEY, String.valueOf(limit));
167             orderBy = UPDATE_ORDER_BY;
168         }
169         try (Cursor cursor =
170                 context.getContentResolver()
171                         .query(
172                                 contactsUriBuilder.build(),
173                                 UPDATE_SELECTION,
174                                 UPDATE_SINCE,
175                                 selectionArgs,
176                                 orderBy)) {
177             if (cursor == null) {
178                 Log.w(TAG, "Failed to get a list of contacts updated since " + sinceFilter);
179                 if (updateStats != null) {
180                     updateStats.mUpdateStatuses.add(ContactsUpdateStats.ERROR_CODE_CP2_NULL_CURSOR);
181                 }
182                 return newTimestamp;
183             }
184 
185             int contactIdIndex = cursor.getColumnIndex(Contacts._ID);
186             int timestampIndex = cursor.getColumnIndex(Contacts.CONTACT_LAST_UPDATED_TIMESTAMP);
187             int numContacts = 0;
188             while (cursor.moveToNext()) {
189                 // Just in case the LIMIT parameter doesn't work in the query to CP2.
190                 if (limit >= 0 && numContacts >= limit) {
191                     break;
192                 }
193 
194                 long contactId = cursor.getLong(contactIdIndex);
195                 contactIds.add(String.valueOf(contactId));
196                 numContacts++;
197                 newTimestamp = Math.max(newTimestamp, cursor.getLong(timestampIndex));
198             }
199 
200             if (LogUtil.DEBUG) {
201                 Log.v(TAG, "Returning " + numContacts + " updated contacts since " + sinceFilter);
202             }
203         } catch (SecurityException
204                 | SQLiteException
205                 | NullPointerException
206                 | NoClassDefFoundError e) {
207             Log.e(TAG, "ContentResolver.query failed to get latest updated contacts.", e);
208             // TODO(b/222126568) consider throwing an exception here. And in the caller it can
209             //  still catch the exception, and based on the states(e.g. whether we query CP2
210             //  successfully before and need to remove some contacts), caller can choose to keep
211             //  doing the update or not.
212             if (updateStats != null) {
213                 updateStats.mUpdateStatuses.add(
214                         ContactsUpdateStats.ERROR_CODE_CP2_RUNTIME_EXCEPTION);
215             }
216         }
217 
218         return newTimestamp;
219     }
220 }
221