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