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