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