Handle AppSearch RESULT_OUT_OF_SPACE error am: 754687a8fc
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/modules/AppSearch/+/17140094
Change-Id: Ieec590d2ca94442036ba8380b86fcc3db83e2f0b
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/service/java/com/android/server/appsearch/AppSearchModule.java b/service/java/com/android/server/appsearch/AppSearchModule.java
index aeafec8..41ec91b 100644
--- a/service/java/com/android/server/appsearch/AppSearchModule.java
+++ b/service/java/com/android/server/appsearch/AppSearchModule.java
@@ -21,7 +21,6 @@
import android.content.Context;
import android.os.Environment;
import android.os.UserHandle;
-import android.provider.DeviceConfig;
import android.util.Log;
import com.android.server.SystemService;
@@ -48,7 +47,8 @@
public static final class Lifecycle extends SystemService {
private AppSearchManagerService mAppSearchManagerService;
- @Nullable private ContactsIndexerManagerService mContactsIndexerManagerService;
+ @Nullable
+ private ContactsIndexerManagerService mContactsIndexerManagerService;
public Lifecycle(Context context) {
super(context);
diff --git a/service/java/com/android/server/appsearch/contactsindexer/AppSearchHelper.java b/service/java/com/android/server/appsearch/contactsindexer/AppSearchHelper.java
index 54b273b..3175ad7 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/AppSearchHelper.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/AppSearchHelper.java
@@ -218,7 +218,7 @@
public void onResult(AppSearchBatchResult<String, Void> result) {
int numDocsSucceeded = result.getSuccesses().size();
int numDocsFailed = result.getFailures().size();
- updateStats.mContactsUpdateCount += numDocsSucceeded;
+ updateStats.mContactsUpdateSucceededCount += numDocsSucceeded;
updateStats.mContactsUpdateFailedCount += numDocsFailed;
if (result.isSuccess()) {
Log.v(TAG,
@@ -234,9 +234,6 @@
updateStats.mUpdateStatuses.add(failure.getResultCode());
}
Log.w(TAG, numDocsFailed + " documents failed to be added in AppSearch.");
- // TODO(b/222187514) we can only have 20,000(default) contacts stored.
- // In order to save the latest contacts, we need to remove the oldest ones
- // in this ELSE. RESULT_OUT_OF_SPACE is the error code for this case.
future.completeExceptionally(new AppSearchException(
firstFailure.getResultCode(), firstFailure.getErrorMessage()));
}
@@ -290,7 +287,7 @@
}
}
}
- updateStats.mContactsDeleteCount += numSuccesses;
+ updateStats.mContactsDeleteSucceededCount += numSuccesses;
updateStats.mContactsDeleteFailedCount += numFailures;
if (firstFailure != null) {
Log.w(TAG, "Failed to delete "
diff --git a/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerConfig.java b/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerConfig.java
index 41baf07..8089c7d 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerConfig.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerConfig.java
@@ -26,24 +26,57 @@
* @hide
*/
public class ContactsIndexerConfig {
- private static final String CONTACTS_INDEXER_ENABLED = "contacts_indexer_enabled";
- private static final String CONTACTS_INSTANT_INDEXING_LIMIT = "contacts_instant_indexing_limit";
- private static final String CONTACTS_FULL_UPDATE_INTERVAL_MILLIS
+ // LIMIT of -1 means no upper bound (see https://www.sqlite.org/lang_select.html)
+ public static final int UPDATE_LIMIT_NONE = -1;
+
+ private static final String KEY_CONTACTS_INSTANT_INDEXING_LIMIT =
+ "contacts_instant_indexing_limit";
+ public static final String KEY_CONTACTS_INDEXER_ENABLED = "contacts_indexer_enabled";
+ public static final String KEY_CONTACTS_FULL_UPDATE_INTERVAL_MILLIS
= "contacts_full_update_interval_millis";
+ public static final String KEY_CONTACTS_FULL_UPDATE_LIMIT =
+ "contacts_indexer_full_update_limit";
+ public static final String KEY_CONTACTS_DELTA_UPDATE_LIMIT =
+ "contacts_indexer_delta_update_limit";
public static boolean isContactsIndexerEnabled() {
- return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_APPSEARCH, CONTACTS_INDEXER_ENABLED,
+ return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_APPSEARCH,
+ KEY_CONTACTS_INDEXER_ENABLED,
/*defaultValue=*/ true);
}
public static int getContactsInstantIndexingLimit() {
return DeviceConfig.getInt(DeviceConfig.NAMESPACE_APPSEARCH,
- CONTACTS_INSTANT_INDEXING_LIMIT, /*defaultValue=*/ 1000);
+ KEY_CONTACTS_INSTANT_INDEXING_LIMIT, /*defaultValue=*/ 1000);
}
public static long getContactsFullUpdateIntervalMillis() {
return DeviceConfig.getLong(DeviceConfig.NAMESPACE_APPSEARCH,
- CONTACTS_FULL_UPDATE_INTERVAL_MILLIS,
+ KEY_CONTACTS_FULL_UPDATE_INTERVAL_MILLIS,
/*defaultValue=*/ TimeUnit.DAYS.toMillis(30));
}
+
+ /**
+ * Returns the maximum number of CP2 contacts indexed during a full update.
+ *
+ * <p>The value will be used as a LIMIT for querying CP2 during full update.
+ */
+ public static int getContactsFullUpdateLimit() {
+ return DeviceConfig.getInt(DeviceConfig.NAMESPACE_APPSEARCH,
+ KEY_CONTACTS_FULL_UPDATE_LIMIT,
+ /*defaultValue=*/ 10_000);
+ }
+
+ /**
+ * Returns the maximum number of CP2 contacts indexed during a delta update.
+ *
+ * <p>The value will be used as a LIMIT for querying CP2 during the delta update.
+ */
+ public static int getContactsDeltaUpdateLimit() {
+ // TODO(b/227419499) Based on the metrics, we can tweak this number. Right now it is same
+ // as the instant indexing limit, which is 1,000. From our stats in GMSCore, 95th
+ // percentile for number of contacts on the device is around 2000 contacts.
+ return DeviceConfig.getInt(DeviceConfig.NAMESPACE_APPSEARCH,
+ KEY_CONTACTS_DELTA_UPDATE_LIMIT, /*defaultValue=*/ 1000);
+ }
}
diff --git a/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerImpl.java b/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerImpl.java
index 61d5c79..da1f6f1 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerImpl.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerImpl.java
@@ -139,6 +139,7 @@
CompletableFuture<Void> batchRemoveFuture = CompletableFuture.completedFuture(null);
int startIndex = 0;
int unWantedSize = unWantedIds.size();
+ updateStats.mTotalContactsToBeDeleted += unWantedSize;
while (startIndex < unWantedSize) {
int endIndex = Math.min(startIndex + NUM_DELETED_CONTACTS_PER_BATCH_FOR_APPSEARCH,
unWantedSize);
@@ -162,6 +163,7 @@
int startIndex = 0;
int wantedIdListSize = wantedContactIds.size();
CompletableFuture<Void> future = CompletableFuture.completedFuture(null);
+ updateStats.mTotalContactsToBeUpdated += wantedIdListSize;
//
// Batch reading the contacts from CP2, and index the created documents to AppSearch
@@ -171,9 +173,6 @@
wantedIdListSize);
Collection<String> currentContactIds = wantedContactIds.subList(startIndex, endIndex);
// Read NUM_CONTACTS_PER_BATCH contacts every time from CP2.
- // TODO(b/203605504) log the total latency for the query once we have the logger
- // configured. Since a big "IN" might cause a slowdown. Also we can make
- // NUM_CONTACTS_PER_BATCH configurable.
String selection = ContactsContract.Data.CONTACT_ID + " IN (" + TextUtils.join(
/*delimiter=*/ ",", currentContactIds) + ")";
startIndex = endIndex;
@@ -202,7 +201,6 @@
// The ContactsProvider sometimes propagates RuntimeExceptions to us
// for when their database fails to open. Behave as if there was no
// ContactsProvider, and flag that we were not successful.
- // TODO(b/203605504) log the error once we have the logger configured.
Log.e(TAG, "ContentResolver.query threw an exception.", e);
updateStats.mUpdateStatuses.add(AppSearchResult.RESULT_INTERNAL_ERROR);
return CompletableFuture.failedFuture(e);
@@ -213,6 +211,15 @@
}
/**
+ * Cancels the {@link #updatePersonCorpusAsync(List, List, ContactsUpdateStats)} in case of
+ * error. This will clean up the states in the batcher so it can get ready for the following
+ * updates.
+ */
+ void cancelUpdatePersonCorpus() {
+ mBatcher.clearBatchedContacts();
+ }
+
+ /**
* Reads through cursor, converts the contacts to AppSearch documents, and indexes the
* documents into AppSearch.
*
@@ -366,6 +373,11 @@
return mPendingIndexContacts.size();
}
+ void clearBatchedContacts() {
+ mPendingDiffContactBuilders.clear();
+ mPendingIndexContacts.clear();
+ }
+
public void add(@NonNull PersonBuilderHelper builderHelper,
@NonNull ContactsUpdateStats updateStats) {
Objects.requireNonNull(builderHelper);
@@ -440,11 +452,11 @@
contactsToBeIndexed.add(person);
} else {
// Fingerprint is same. So this update is skipped.
- ++updateStats.mContactsSkippedCount;
+ ++updateStats.mContactsUpdateSkippedCount;
}
} else {
// New contact.
- ++updateStats.mContactsInsertedCount;
+ ++updateStats.mNewContactsToBeUpdated;
contactsToBeIndexed.add(builderHelper.buildPerson());
}
}
@@ -457,10 +469,13 @@
/** Flushes the contacts batched in {@link #mPendingIndexContacts} to AppSearch. */
private CompletableFuture<Void> flushPendingIndexAsync(
@NonNull ContactsUpdateStats updateStats) {
- CompletableFuture<Void> future =
- mAppSearchHelper.indexContactsAsync(mPendingIndexContacts, updateStats);
- mPendingIndexContacts.clear();
- return future;
+ if (mPendingIndexContacts.size() > 0) {
+ CompletableFuture<Void> future =
+ mAppSearchHelper.indexContactsAsync(mPendingIndexContacts, updateStats);
+ mPendingIndexContacts.clear();
+ return future;
+ }
+ return CompletableFuture.completedFuture(null);
}
}
}
diff --git a/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerManagerService.java b/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerManagerService.java
index 75032ad..f83133a 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerManagerService.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerManagerService.java
@@ -33,6 +33,7 @@
import android.util.Log;
import android.util.SparseArray;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.LocalManagerRegistry;
import com.android.server.SystemService;
import com.android.server.appsearch.AppSearchModule;
@@ -199,7 +200,8 @@
}
class LocalService {
- void doFullUpdateForUser(@UserIdInt int userId, CancellationSignal signal) {
+ void doFullUpdateForUser(@UserIdInt int userId, @NonNull CancellationSignal signal) {
+ Objects.requireNonNull(signal);
synchronized (mContactsIndexersLocked) {
ContactsIndexerUserInstance instance = mContactsIndexersLocked.get(userId);
if (instance != null) {
diff --git a/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerUserInstance.java b/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerUserInstance.java
index 533e9f0..29fb34d 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerUserInstance.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerUserInstance.java
@@ -17,6 +17,7 @@
package com.android.server.appsearch.contactsindexer;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.appsearch.AppSearchResult;
import android.content.ContentResolver;
import android.content.Context;
@@ -63,6 +64,7 @@
// Used for batching/throttling the contact change notification so we won't schedule too many
// delta updates.
private final AtomicBoolean mDeltaUpdatePending = new AtomicBoolean(/*initialValue=*/ false);
+
private final AppSearchHelper mAppSearchHelper;
private final ContactsIndexerImpl mContactsIndexerImpl;
@@ -92,7 +94,8 @@
@VisibleForTesting
@NonNull
/*package*/ static ContactsIndexerUserInstance createInstance(@NonNull Context context,
- @NonNull File contactsDir, @NonNull ExecutorService executorService) {
+ @NonNull File contactsDir,
+ @NonNull ExecutorService executorService) {
Objects.requireNonNull(context);
Objects.requireNonNull(contactsDir);
Objects.requireNonNull(executorService);
@@ -109,11 +112,11 @@
/**
* Constructs a {@link ContactsIndexerUserInstance}.
*
- * @param context Context object passed from
- * {@link ContactsIndexerManagerService}
- * @param dataDir data directory for storing contacts indexer state.
- * @param singleThreadedExecutor an {@link ExecutorService} with at most one thread to ensure
- * the thread safety of this class.
+ * @param context Context object passed from
+ * {@link ContactsIndexerManagerService}
+ * @param dataDir data directory for storing contacts indexer state.
+ * @param singleThreadedExecutor an {@link ExecutorService} with at most one thread to ensure
+ * the thread safety of this class.
*/
private ContactsIndexerUserInstance(@NonNull Context context, @NonNull File dataDir,
@NonNull AppSearchHelper appSearchHelper,
@@ -192,9 +195,7 @@
*
* @param signal Used to indicate if the full update task should be cancelled.
*/
- public void doFullUpdateAsync(@NonNull CancellationSignal signal) {
- Objects.requireNonNull(signal);
- // TODO(b/222126568): log stats
+ public void doFullUpdateAsync(@Nullable CancellationSignal signal) {
mSingleThreadedExecutor.execute(() -> {
ContactsUpdateStats updateStats = new ContactsUpdateStats();
doFullUpdateInternalAsync(signal, updateStats);
@@ -203,18 +204,19 @@
@VisibleForTesting
CompletableFuture<Void> doFullUpdateInternalAsync(
- @NonNull CancellationSignal signal, @NonNull ContactsUpdateStats updateStats) {
+ @Nullable CancellationSignal signal, @NonNull ContactsUpdateStats updateStats) {
// TODO(b/203605504): handle cancellation signal to abort the job.
long currentTimeMillis = System.currentTimeMillis();
updateStats.mUpdateType = ContactsUpdateStats.FULL_UPDATE;
- updateStats.mUpdateStartTimeMillis = currentTimeMillis;
+ updateStats.mUpdateAndDeleteStartTimeMillis = currentTimeMillis;
List<String> cp2ContactIds = new ArrayList<>();
// Get a list of all contact IDs from CP2. Ignore the return value which denotes the
// most recent updated timestamp.
// TODO(b/203605504): reconsider whether the most recent
// updated and deleted timestamps are useful.
- ContactsProviderUtil.getUpdatedContactIds(mContext, /*sinceFilter=*/ 0, cp2ContactIds,
+ ContactsProviderUtil.getUpdatedContactIds(mContext, /*sinceFilter=*/ 0,
+ ContactsIndexerConfig.getContactsFullUpdateLimit(), cp2ContactIds,
updateStats);
return mAppSearchHelper.getAllContactIdsAsync()
.thenCompose(appsearchContactIds -> {
@@ -230,6 +232,8 @@
}).handle((x, t) -> {
if (t != null) {
Log.w(TAG, "Failed to perform full update", t);
+ // Just clear all the remaining contacts in case of error.
+ mContactsIndexerImpl.cancelUpdatePersonCorpus();
if (updateStats.mUpdateStatuses.isEmpty()
&& updateStats.mDeleteStatuses.isEmpty()) {
// Somehow this error is not reflected in the stats, and
@@ -282,9 +286,10 @@
ContactsUpdateStats updateStats = new ContactsUpdateStats();
// TODO(b/226489369): apply instant indexing limit on CP2 changes also?
// TODO(b/222126568): refactor doDeltaUpdateAsync() to return a future value of
- // ContactsUpdateStats so that it can be checked and logged here, instead of the
- // placeholder exceptionally() block that only logs to the console.
- doDeltaUpdateAsync(/*indexingLimit=*/ -1, updateStats).exceptionally(t -> {
+ // ContactsUpdateStats so that it can be checked and logged here, instead of the
+ // placeholder exceptionally() block that only logs to the console.
+ doDeltaUpdateAsync(ContactsIndexerConfig.getContactsDeltaUpdateLimit(),
+ updateStats).exceptionally(t -> {
Log.d(TAG, "Failed to index CP2 change", t);
return null;
});
@@ -310,7 +315,7 @@
// flag is reset.
mDeltaUpdatePending.set(false);
updateStats.mUpdateType = ContactsUpdateStats.DELTA_UPDATE;
- updateStats.mUpdateStartTimeMillis = System.currentTimeMillis();
+ updateStats.mUpdateAndDeleteStartTimeMillis = System.currentTimeMillis();
long lastDeltaUpdateTimestampMillis = mSettings.getLastDeltaUpdateTimestampMillis();
long lastDeltaDeleteTimestampMillis = mSettings.getLastDeltaDeleteTimestampMillis();
Log.d(TAG, "previous timestamps --"
@@ -337,6 +342,8 @@
.handle((x, t) -> {
if (t != null) {
Log.w(TAG, "Failed to perform delta update", t);
+ // Just clear all the remaining contacts in case of error.
+ mContactsIndexerImpl.cancelUpdatePersonCorpus();
if (updateStats.mUpdateStatuses.isEmpty()
&& updateStats.mDeleteStatuses.isEmpty()) {
// Somehow this error is not reflected in the stats, and
@@ -350,7 +357,7 @@
Log.d(TAG, "updated timestamps --"
+ " lastDeltaUpdateTimestampMillis: "
+ mostRecentContactLastUpdateTimestampMillis
- + " lastDeltaDeleeteTimestampMillis: "
+ + " lastDeltaDeleteTimestampMillis: "
+ mostRecentContactDeletedTimestampMillis);
mSettings.setLastDeltaUpdateTimestampMillis(
mostRecentContactLastUpdateTimestampMillis);
@@ -358,6 +365,17 @@
mostRecentContactDeletedTimestampMillis);
persistSettings();
logStats(updateStats);
+ if (updateStats.mUpdateStatuses.contains(AppSearchResult.RESULT_OUT_OF_SPACE)) {
+ // Some indexing failed due to OUT_OF_SPACE from AppSearch. We can simply
+ // schedule a full update so we can trim the Person corpus in AppSearch
+ // to make some room for delta update. We need to monitor the failure
+ // count and reasons for indexing during full update to see if that limit
+ // (10,000) is too big right now, considering we are sharing this limit
+ // with any AppSearch clients, e.g. ShortcutManager, in the system server.
+ ContactsIndexerMaintenanceService.scheduleFullUpdateJob(mContext,
+ mContext.getUser().getIdentifier());
+ }
+
return null;
});
}
@@ -366,7 +384,7 @@
private void logStats(@NonNull ContactsUpdateStats updateStats) {
int totalUpdateLatency =
(int) (System.currentTimeMillis()
- - updateStats.mUpdateStartTimeMillis);
+ - updateStats.mUpdateAndDeleteStartTimeMillis);
// Finalize status code for update and delete.
if (updateStats.mUpdateStatuses.isEmpty()) {
// SUCCESS if no error found.
@@ -376,6 +394,17 @@
// SUCCESS if no error found.
updateStats.mDeleteStatuses.add(AppSearchResult.RESULT_OK);
}
+
+ // Get the accurate count for failed cases. The current failed count doesn't include
+ // the contacts skipped due to failures in previous batches. Once a batch fails, all the
+ // following batches will be skipped. The contacts in those batches should be counted as
+ // failure as well.
+ updateStats.mContactsUpdateFailedCount =
+ updateStats.mTotalContactsToBeUpdated - updateStats.mContactsUpdateSucceededCount
+ - updateStats.mContactsUpdateSkippedCount;
+ updateStats.mContactsDeleteFailedCount =
+ updateStats.mTotalContactsToBeDeleted - updateStats.mContactsDeleteSucceededCount;
+
int[] updateStatusArr = new int[updateStats.mUpdateStatuses.size()];
int[] deleteStatusArr = new int[updateStats.mDeleteStatuses.size()];
int updateIdx = 0;
@@ -394,13 +423,12 @@
totalUpdateLatency,
updateStatusArr,
deleteStatusArr,
- updateStats.mContactsInsertedCount,
- updateStats.mContactsUpdateCount,
- updateStats.mContactsDeleteCount,
- updateStats.mContactsSkippedCount,
+ updateStats.mNewContactsToBeUpdated,
+ updateStats.mContactsUpdateSucceededCount,
+ updateStats.mContactsDeleteSucceededCount,
+ updateStats.mContactsUpdateSkippedCount,
updateStats.mContactsUpdateFailedCount,
updateStats.mContactsDeleteFailedCount);
-
}
/**
diff --git a/service/java/com/android/server/appsearch/contactsindexer/ContactsProviderUtil.java b/service/java/com/android/server/appsearch/contactsindexer/ContactsProviderUtil.java
index ed154df..ff32bf1 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/ContactsProviderUtil.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/ContactsProviderUtil.java
@@ -126,25 +126,15 @@
}
/**
- * Gets the ids for updated contacts from certain timestamp.
+ * Returns a list of IDs, within given limit, of contacts updated since given timestamp.
*
* @param sinceFilter timestamp (milliseconds since epoch) from which ids of recently updated
* contacts should be returned.
* @param contactIds the Set passed in to hold the recently updated contacts.
+ * @param limit the maximum number of contacts fetched from CP2. No limit will be set if
+ * the value is {@link ContactsIndexerConfig#UPDATE_LIMIT_NONE}.
* @return the timestamp for the contact most recently updated.
*/
- public static long getUpdatedContactIds(@NonNull Context context, long sinceFilter,
- @NonNull List<String> contactIds, @Nullable ContactsUpdateStats updateStats) {
- // LIMIT of -1 means no upper bound (see https://www.sqlite.org/lang_select.html)
- return getUpdatedContactIds(context, sinceFilter, /*limit=*/-1, contactIds,
- updateStats);
- }
-
- /**
- * Returns a list of IDs, within given limit, of contacts updated since given timestamp.
- *
- * <p>List of contact IDs are returned in the order of increasing contact last update timestamp.
- */
public static long getUpdatedContactIds(@NonNull Context context, long sinceFilter, int limit,
@NonNull List<String> contactIds, @Nullable ContactsUpdateStats updateStats) {
Objects.requireNonNull(context);
@@ -156,31 +146,37 @@
Uri.Builder contactsUriBuilder = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
ContactsContract.DIRECTORY_PARAM_KEY,
String.valueOf(ContactsContract.Directory.DEFAULT));
- if (limit > 0) {
+ String orderBy = null;
+ if (limit >= 0) {
contactsUriBuilder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
String.valueOf(limit));
+ orderBy = UPDATE_ORDER_BY;
}
try (Cursor cursor = context.getContentResolver().query(
contactsUriBuilder.build(),
UPDATE_SELECTION,
UPDATE_SINCE, selectionArgs,
- UPDATE_ORDER_BY)) {
+ orderBy)) {
if (cursor == null) {
Log.w(TAG, "Failed to get a list of contacts updated since " + sinceFilter);
return newTimestamp;
}
int contactIdIndex = cursor.getColumnIndex(Contacts._ID);
- int timestampIndex = cursor.getColumnIndex(Contacts.CONTACT_LAST_UPDATED_TIMESTAMP);
+ int timestampIndex = cursor.getColumnIndex(
+ Contacts.CONTACT_LAST_UPDATED_TIMESTAMP);
int numContacts = 0;
while (cursor.moveToNext()) {
+ // Just in case the LIMIT parameter doesn't work in the query to CP2.
+ if (limit >= 0 && numContacts >= limit) {
+ break;
+ }
+
long contactId = cursor.getLong(contactIdIndex);
contactIds.add(String.valueOf(contactId));
numContacts++;
newTimestamp = Math.max(newTimestamp, cursor.getLong(timestampIndex));
}
- // Reverse the IDs in the list to be in increasing contact last updated order.
- Collections.reverse(contactIds);
Log.v(TAG, "Returning " + numContacts + " updated contacts since " + sinceFilter);
} catch (SecurityException |
diff --git a/service/java/com/android/server/appsearch/contactsindexer/ContactsUpdateStats.java b/service/java/com/android/server/appsearch/contactsindexer/ContactsUpdateStats.java
index 3cab94e..6557183 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/ContactsUpdateStats.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/ContactsUpdateStats.java
@@ -70,34 +70,53 @@
@AppSearchResult.ResultCode
Set<Integer> mDeleteStatuses = new ArraySet<>();
- long mUpdateStartTimeMillis;
+ // Start time in millis for update and delete.
+ long mUpdateAndDeleteStartTimeMillis;
- // # of contacts failed to be inserted or updated.
+
+ //
+ // Update for both old and new contacts(a.k.a insertion).
+ //
+ // # of old and new contacts failed to be updated.
int mContactsUpdateFailedCount;
- // # of contacts failed to be deleted.
+ // # of old and new contacts succeeds to be updated.
+ int mContactsUpdateSucceededCount;
+ // # of contacts update skipped due to NO significant change during the update.
+ int mContactsUpdateSkippedCount;
+ // Total # of old and new contacts to be updated.
+ // It should equal to
+ // mContactsUpdateFailedCount + mContactsUpdateSucceededCount + mContactsUpdateSkippedCount
+ int mTotalContactsToBeUpdated;
+ // Among the succeeded and failed contacts updates, how many of them are for the new contacts
+ // currently NOT available in AppSearch.
+ int mNewContactsToBeUpdated;
+
+ //
+ // Deletion for old documents.
+ //
+ // # of old contacts failed to be deleted.
int mContactsDeleteFailedCount;
-
- // # of new contacts to be inserted
- int mContactsInsertedCount;
- // # of contacts skipped due to no significant change
- int mContactsSkippedCount;
- // # of contacts successfully inserted or updated.
- int mContactsUpdateCount;
-
- // # of contacts deleted.
- int mContactsDeleteCount;
+ // # of old contacts succeeds to be deleted.
+ int mContactsDeleteSucceededCount;
+ // Total # of old contacts to be deleted. It should equal to
+ // mContactsDeleteFailedCount + mContactsDeleteSucceededCount
+ int mTotalContactsToBeDeleted;
public void clear() {
mUpdateType = UNKNOWN_UPDATE_TYPE;
mUpdateStatuses.clear();
mDeleteStatuses.clear();
- mUpdateStartTimeMillis = 0;
+ mUpdateAndDeleteStartTimeMillis = 0;
+ // Update for old and new contacts
mContactsUpdateFailedCount = 0;
+ mContactsUpdateSucceededCount = 0;
+ mContactsUpdateSkippedCount = 0;
+ mNewContactsToBeUpdated = 0;
+ mTotalContactsToBeUpdated = 0;
+ // delete for old contacts
mContactsDeleteFailedCount = 0;
- mContactsInsertedCount = 0;
- mContactsSkippedCount = 0;
- mContactsUpdateCount = 0;
- mContactsDeleteCount = 0;
+ mContactsDeleteSucceededCount = 0;
+ mTotalContactsToBeDeleted = 0;
}
@NonNull
@@ -105,12 +124,14 @@
return "UpdateType: " + mUpdateType
+ ", UpdateStatus: " + mUpdateStatuses.toString()
+ ", DeleteStatus: " + mDeleteStatuses.toString()
- + ", UpdateStartTimeMillis: " + mUpdateStartTimeMillis
- + ", UpdateFailedCount: " + mContactsUpdateFailedCount
- + ", deleteFailedCount: " + mContactsDeleteFailedCount
- + ", ContactsInsertedCount: " + mContactsInsertedCount
- + ", ContactsSkippedCount: " + mContactsSkippedCount
- + ", ContactsUpdateCount: " + mContactsUpdateCount
- + ", ContactsDeleteCount: " + mContactsDeleteCount;
+ + ", UpdateAndDeleteStartTimeMillis: " + mUpdateAndDeleteStartTimeMillis
+ + ", ContactsUpdateFailedCount: " + mContactsUpdateFailedCount
+ + ", ContactsUpdateSucceededCount: " + mContactsUpdateSucceededCount
+ + ", NewContactsToBeUpdated: " + mNewContactsToBeUpdated
+ + ", ContactsUpdateSkippedCount: " + mContactsUpdateSkippedCount
+ + ", TotalContactsToBeUpdated: " + mTotalContactsToBeUpdated
+ + ", ContactsDeleteFailedCount: " + mContactsDeleteFailedCount
+ + ", ContactsDeleteSucceededCount: " + mContactsDeleteSucceededCount
+ + ", TotalContactsToBeDeleted: " + mTotalContactsToBeDeleted;
}
}
\ No newline at end of file
diff --git a/service/java/com/android/server/appsearch/contactsindexer/PersonBuilderHelper.java b/service/java/com/android/server/appsearch/contactsindexer/PersonBuilderHelper.java
index 0cddb53..73102c5 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/PersonBuilderHelper.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/PersonBuilderHelper.java
@@ -42,8 +42,6 @@
*
* @hide
*/
-// TODO(b/203605504) We can also make it only generates a list of contact points. And move the
-// building of a Person out to the caller.
public final class PersonBuilderHelper {
static final String TAG = "PersonBuilderHelper";
static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
diff --git a/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/ContactsIndexerImplTest.java b/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/ContactsIndexerImplTest.java
index 4f267b0..64dad40 100644
--- a/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/ContactsIndexerImplTest.java
+++ b/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/ContactsIndexerImplTest.java
@@ -97,7 +97,8 @@
List<String> unWantedContactIds = new ArrayList<>();
lastUpdatedTimestamp = ContactsProviderUtil.getUpdatedContactIds(mContext,
- lastUpdatedTimestamp, wantedContactIds, /*stats=*/ null);
+ lastUpdatedTimestamp, ContactsIndexerConfig.UPDATE_LIMIT_NONE,
+ wantedContactIds, /*stats=*/ null);
lastDeletedTimestamp = ContactsProviderUtil.getDeletedContactIds(mContext,
lastDeletedTimestamp, unWantedContactIds, /*stats=*/ null);
indexerImpl.updatePersonCorpusAsync(wantedContactIds, unWantedContactIds,
diff --git a/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/ContactsIndexerUserInstanceTest.java b/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/ContactsIndexerUserInstanceTest.java
index 75b4992..a8139b1 100644
--- a/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/ContactsIndexerUserInstanceTest.java
+++ b/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/ContactsIndexerUserInstanceTest.java
@@ -16,6 +16,8 @@
package com.android.server.appsearch.contactsindexer;
+import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -23,12 +25,19 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
+import android.app.UiAutomation;
import android.app.appsearch.AppSearchManager;
import android.app.appsearch.AppSearchResult;
import android.app.appsearch.AppSearchSessionShim;
+import android.app.appsearch.GlobalSearchSessionShim;
import android.app.appsearch.SearchSpec;
import android.app.appsearch.SetSchemaRequest;
+import android.app.appsearch.observer.DocumentChangeInfo;
+import android.app.appsearch.observer.ObserverCallback;
+import android.app.appsearch.observer.ObserverSpec;
+import android.app.appsearch.observer.SchemaChangeInfo;
import android.app.appsearch.testutil.AppSearchSessionShimImpl;
+import android.app.appsearch.testutil.GlobalSearchSessionShimImpl;
import android.app.job.JobScheduler;
import android.content.ContentResolver;
import android.content.ContentUris;
@@ -37,11 +46,14 @@
import android.os.CancellationSignal;
import android.os.PersistableBundle;
import android.provider.ContactsContract;
+import android.provider.DeviceConfig;
import android.test.ProviderTestCase2;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.server.appsearch.FrameworkAppSearchConfig;
import com.android.server.appsearch.contactsindexer.appsearchtypes.Person;
import org.junit.After;
@@ -56,8 +68,10 @@
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nullable;
@@ -72,7 +86,6 @@
public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
private final ExecutorService mSingleThreadedExecutor = Executors.newSingleThreadExecutor();
-
private ContextWrapper mContextWrapper;
private File mContactsDir;
private File mSettingsFile;
@@ -92,11 +105,9 @@
// Setup the file path to the persisted data
mContactsDir = new File(mTemporaryFolder.newFolder(), "appsearch/contacts");
mSettingsFile = new File(mContactsDir, ContactsIndexerSettings.SETTINGS_FILE_NAME);
-
mContextWrapper = new ContextWrapper(ApplicationProvider.getApplicationContext());
mContextWrapper.setContentResolver(getMockContentResolver());
mContext = mContextWrapper;
-
mSpecForQueryAllContacts = new SearchSpec.Builder().addFilterSchemas(
Person.SCHEMA_TYPE).addProjection(Person.SCHEMA_TYPE,
Arrays.asList(Person.PERSON_PROPERTY_NAME))
@@ -105,7 +116,6 @@
mInstance = ContactsIndexerUserInstance.createInstance(mContext, mContactsDir,
mSingleThreadedExecutor);
-
mUpdateStats = new ContactsUpdateStats();
}
@@ -222,10 +232,12 @@
assertThat(mUpdateStats.mContactsUpdateFailedCount).isEqualTo(0);
// NOT_FOUND does not count as error.
assertThat(mUpdateStats.mContactsDeleteFailedCount).isEqualTo(0);
- assertThat(mUpdateStats.mContactsInsertedCount).isEqualTo(250);
- assertThat(mUpdateStats.mContactsSkippedCount).isEqualTo(0);
- assertThat(mUpdateStats.mContactsUpdateCount).isEqualTo(250);
- assertThat(mUpdateStats.mContactsDeleteCount).isEqualTo(0);
+ assertThat(mUpdateStats.mNewContactsToBeUpdated).isEqualTo(250);
+ assertThat(mUpdateStats.mContactsUpdateSkippedCount).isEqualTo(0);
+ assertThat(mUpdateStats.mTotalContactsToBeUpdated).isEqualTo(250);
+ assertThat(mUpdateStats.mContactsUpdateSucceededCount).isEqualTo(250);
+ assertThat(mUpdateStats.mTotalContactsToBeDeleted).isEqualTo(0);
+ assertThat(mUpdateStats.mContactsDeleteSucceededCount).isEqualTo(0);
}
@Test
@@ -281,10 +293,23 @@
assertThat(contactIds.size()).isEqualTo(6);
assertThat(contactIds).containsNoneOf("2", "3", "5", "7");
- // TODO(b/222126568): verify state using logged events instead
PersistableBundle settingsBundle = ContactsIndexerSettings.readBundle(mSettingsFile);
assertThat(settingsBundle.getLong(ContactsIndexerSettings.LAST_DELTA_UPDATE_TIMESTAMP_KEY))
.isAtLeast(timeBeforeDeltaChangeNotification);
+ // check stats
+ assertThat(mUpdateStats.mUpdateType).isEqualTo(ContactsUpdateStats.DELTA_UPDATE);
+ assertThat(mUpdateStats.mUpdateStatuses).hasSize(1);
+ assertThat(mUpdateStats.mUpdateStatuses).containsExactly(AppSearchResult.RESULT_OK);
+ assertThat(mUpdateStats.mDeleteStatuses).hasSize(1);
+ assertThat(mUpdateStats.mDeleteStatuses).containsExactly(AppSearchResult.RESULT_OK);
+ assertThat(mUpdateStats.mContactsUpdateFailedCount).isEqualTo(0);
+ assertThat(mUpdateStats.mContactsDeleteFailedCount).isEqualTo(0);
+ assertThat(mUpdateStats.mNewContactsToBeUpdated).isEqualTo(10);
+ assertThat(mUpdateStats.mContactsUpdateSkippedCount).isEqualTo(0);
+ assertThat(mUpdateStats.mTotalContactsToBeUpdated).isEqualTo(10);
+ assertThat(mUpdateStats.mContactsUpdateSucceededCount).isEqualTo(10);
+ assertThat(mUpdateStats.mTotalContactsToBeDeleted).isEqualTo(4);
+ assertThat(mUpdateStats.mContactsDeleteSucceededCount).isEqualTo(4);
}
@Test
@@ -330,12 +355,15 @@
assertThat(mUpdateStats.mDeleteStatuses).hasSize(1);
assertThat(mUpdateStats.mDeleteStatuses).containsExactly(AppSearchResult.RESULT_OK);
assertThat(mUpdateStats.mContactsUpdateFailedCount).isEqualTo(0);
- // NOT_FOUND does not count as error.
- assertThat(mUpdateStats.mContactsDeleteFailedCount).isEqualTo(0);
- assertThat(mUpdateStats.mContactsInsertedCount).isEqualTo(6);
- assertThat(mUpdateStats.mContactsSkippedCount).isEqualTo(0);
- assertThat(mUpdateStats.mContactsUpdateCount).isEqualTo(6);
- assertThat(mUpdateStats.mContactsDeleteCount).isEqualTo(0);
+ // 4 contacts deleted in CP2, but we don't have those in AppSearch. So we will get
+ // NOT_FOUND. We don't treat the NOT_FOUND as failures, so the status code is still OK.
+ assertThat(mUpdateStats.mContactsDeleteFailedCount).isEqualTo(4);
+ assertThat(mUpdateStats.mNewContactsToBeUpdated).isEqualTo(6);
+ assertThat(mUpdateStats.mContactsUpdateSkippedCount).isEqualTo(0);
+ assertThat(mUpdateStats.mTotalContactsToBeUpdated).isEqualTo(6);
+ assertThat(mUpdateStats.mContactsUpdateSucceededCount).isEqualTo(6);
+ assertThat(mUpdateStats.mTotalContactsToBeDeleted).isEqualTo(4);
+ assertThat(mUpdateStats.mContactsDeleteSucceededCount).isEqualTo(0);
}
@Test
@@ -390,10 +418,83 @@
assertThat(mUpdateStats.mContactsUpdateFailedCount).isEqualTo(0);
// NOT_FOUND does not count as error.
assertThat(mUpdateStats.mContactsDeleteFailedCount).isEqualTo(0);
- assertThat(mUpdateStats.mContactsInsertedCount).isEqualTo(5);
- assertThat(mUpdateStats.mContactsSkippedCount).isEqualTo(0);
- assertThat(mUpdateStats.mContactsUpdateCount).isEqualTo(5);
- assertThat(mUpdateStats.mContactsDeleteCount).isEqualTo(4);
+ assertThat(mUpdateStats.mNewContactsToBeUpdated).isEqualTo(5);
+ assertThat(mUpdateStats.mContactsUpdateSkippedCount).isEqualTo(0);
+ assertThat(mUpdateStats.mTotalContactsToBeUpdated).isEqualTo(5);
+ assertThat(mUpdateStats.mContactsUpdateSucceededCount).isEqualTo(5);
+ assertThat(mUpdateStats.mTotalContactsToBeDeleted).isEqualTo(4);
+ assertThat(mUpdateStats.mContactsDeleteSucceededCount).isEqualTo(4);
+ }
+
+ @Test
+ public void testDeltaUpdate_outOfSpaceError_fullUpdateScheduled() throws Exception {
+ UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+ int maxDocumentCountBeforeTest = FrameworkAppSearchConfig.getInstance(
+ mSingleThreadedExecutor).getCachedLimitConfigMaxDocumentCount();
+ try {
+ uiAutomation.adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG);
+ int totalContactCount = 250;
+ int maxDocumentCount = 100;
+ // Override the configs in AppSearch. This is hard to be mocked since we are not testing
+ // AppSearch here.
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
+ FrameworkAppSearchConfig.KEY_LIMIT_CONFIG_MAX_DOCUMENT_COUNT,
+ String.valueOf(maxDocumentCount), false);
+ // Cancel any existing jobs.
+ ContactsIndexerMaintenanceService.cancelFullUpdateJob(mContext, mContext.getUserId());
+
+ JobScheduler mockJobScheduler = mock(JobScheduler.class);
+ mContextWrapper.setJobScheduler(mockJobScheduler);
+
+ // We are trying to index 250 contacts, but our max documentCount is 100. So we would
+ // index 100 contacts, reach the limit, and trigger a full update.
+ CountDownLatch latch = new CountDownLatch(maxDocumentCount);
+ GlobalSearchSessionShim shim =
+ GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext).get();
+ ObserverCallback callback = new ObserverCallback() {
+ @Override
+ public void onSchemaChanged(SchemaChangeInfo changeInfo) {
+ // Do nothing
+ }
+
+ @Override
+ public void onDocumentChanged(DocumentChangeInfo changeInfo) {
+ for (int i = 0; i < changeInfo.getChangedDocumentIds().size(); i++) {
+ latch.countDown();
+ }
+ }
+ };
+ shim.registerObserverCallback(mContext.getPackageName(),
+ new ObserverSpec.Builder().addFilterSchemas("builtin:Person").build(),
+ mSingleThreadedExecutor,
+ callback);
+
+ long timeBeforeDeltaChangeNotification = System.currentTimeMillis();
+ // Insert contacts to trigger delta update.
+ ContentResolver resolver = mContext.getContentResolver();
+ ContentValues dummyValues = new ContentValues();
+ for (int i = 0; i < totalContactCount; i++) {
+ resolver.insert(ContactsContract.Contacts.CONTENT_URI, dummyValues);
+ }
+
+ executeAndWaitForCompletion(
+ mInstance.doDeltaUpdateAsync(ContactsIndexerConfig.UPDATE_LIMIT_NONE,
+ mUpdateStats),
+ mSingleThreadedExecutor);
+ latch.await(30L, TimeUnit.SECONDS);
+
+ // Verify the full update job is scheduled due to out_of_space.
+ verify(mockJobScheduler).schedule(any());
+ PersistableBundle settingsBundle = ContactsIndexerSettings.readBundle(mSettingsFile);
+ assertThat(
+ settingsBundle.getLong(ContactsIndexerSettings.LAST_DELTA_UPDATE_TIMESTAMP_KEY))
+ .isAtLeast(timeBeforeDeltaChangeNotification);
+ } finally {
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
+ FrameworkAppSearchConfig.KEY_LIMIT_CONFIG_MAX_DOCUMENT_COUNT,
+ String.valueOf(maxDocumentCountBeforeTest), false);
+ uiAutomation.dropShellPermissionIdentity();
+ }
}
/**
@@ -403,17 +504,17 @@
* executor, and wait for its execution. The second step is to wait for the completion of the
* stage itself.
*/
- private void executeAndWaitForCompletion(CompletionStage<Void> stage, ExecutorService executor)
+ private <T> T executeAndWaitForCompletion(CompletionStage<T> stage, ExecutorService executor)
throws Exception {
- AtomicReference<CompletableFuture<Void>> future = new AtomicReference<>(
+ AtomicReference<CompletableFuture<T>> future = new AtomicReference<>(
CompletableFuture.completedFuture(null));
executor.submit(() -> {
// Chain the given stage inside the runnable task so that it executes on the executor.
- CompletableFuture<Void> chainedFuture = future.get().thenCompose(x -> stage);
+ CompletableFuture<T> chainedFuture = future.get().thenCompose(x -> stage);
future.set(chainedFuture);
}).get();
// Wait for the task to complete on the executor, and wait for the stage to complete also.
- future.get().get();
+ return future.get().get();
}
static final class ContextWrapper extends android.content.ContextWrapper {
diff --git a/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/ContactsProviderUtilTest.java b/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/ContactsProviderUtilTest.java
index bbe2fe0..93b1c07 100644
--- a/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/ContactsProviderUtilTest.java
+++ b/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/ContactsProviderUtilTest.java
@@ -68,7 +68,7 @@
List<String> ids = new ArrayList<>();
long lastUpdatedTime = ContactsProviderUtil.getUpdatedContactIds(mContext,
- /*sinceFilter=*/ 0, ids, /*stats=*/ null);
+ /*sinceFilter=*/ 0, ContactsIndexerConfig.UPDATE_LIMIT_NONE, ids, /*stats=*/ null);
assertThat(lastUpdatedTime).isEqualTo(
getProvider().getMostRecentContactUpdateTimestampMillis());
@@ -86,7 +86,7 @@
List<String> ids = new ArrayList<>();
long lastUpdatedTime = ContactsProviderUtil.getUpdatedContactIds(mContext,
/*sinceFilter=*/ getProvider().getMostRecentContactUpdateTimestampMillis(),
- ids, /*stats=*/ null);
+ ContactsIndexerConfig.UPDATE_LIMIT_NONE, ids, /*stats=*/ null);
assertThat(lastUpdatedTime).isEqualTo(
getProvider().getMostRecentContactUpdateTimestampMillis());
@@ -108,7 +108,8 @@
List<String> ids = new ArrayList<>();
long lastUpdatedTime = ContactsProviderUtil.getUpdatedContactIds(mContext,
- /*sinceFilter=*/ firstUpdateTimestamp, ids, /*stats=*/ null);
+ /*sinceFilter=*/ firstUpdateTimestamp, ContactsIndexerConfig.UPDATE_LIMIT_NONE,
+ ids, /*stats=*/ null);
assertThat(lastUpdatedTime).isEqualTo(
getProvider().getMostRecentContactUpdateTimestampMillis());