From e55cf2579c833f3be74775226dfb2f9337441cbf Mon Sep 17 00:00:00 2001 From: Alexander Dorokhine Date: Fri, 2 Jul 2021 14:50:05 -0700 Subject: [PATCH] Cache nextPageTokens that a package can use. A user-package can only manipulate their own nextPageTokens (i.e. advance or invalidate it) otherwise some other package could affect the search results of a package. Because we check at the user-package level, this still allows a package to manipulate nextPageTokens that were returned for different databases. This isn't recommended, but not a security risk since it's the package's own data. Note that manipulating nextPageTokens isn't available through AppSearch's public API. This would be if someone were circumventing the normal AppSearchSession. Bug: 187972715 Test: AppSearchImplTest Change-Id: I67a22f3ae171ea2886eb89dcf493286a8421408d --- .../appsearch/AppSearchManagerService.java | 6 +- .../external/localstorage/AppSearchImpl.java | 96 +++- .../localstorage/AppSearchImplTest.java | 444 +++++++++++++++++- 3 files changed, 526 insertions(+), 20 deletions(-) diff --git a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java index ec37c3f68aaa8..85a9cce53c94d 100644 --- a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java +++ b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java @@ -777,7 +777,7 @@ public class AppSearchManagerService extends SystemService { AppSearchUserInstance instance = mAppSearchUserInstanceManager.getUserInstance(callingUser); SearchResultPage searchResultPage = - instance.getAppSearchImpl().getNextPage(nextPageToken); + instance.getAppSearchImpl().getNextPage(packageName, nextPageToken); invokeCallbackOnResult( callback, AppSearchResult.newSuccessfulResult(searchResultPage.getBundle())); @@ -803,7 +803,7 @@ public class AppSearchManagerService extends SystemService { verifyNotInstantApp(userContext, packageName); AppSearchUserInstance instance = mAppSearchUserInstanceManager.getUserInstance(callingUser); - instance.getAppSearchImpl().invalidateNextPageToken(nextPageToken); + instance.getAppSearchImpl().invalidateNextPageToken(packageName, nextPageToken); } catch (Throwable t) { Log.e(TAG, "Unable to invalidate the query page token", t); } @@ -853,7 +853,7 @@ public class AppSearchManagerService extends SystemService { .getGenericDocument().getBundle()); } searchResultPage = instance.getAppSearchImpl().getNextPage( - searchResultPage.getNextPageToken()); + packageName, searchResultPage.getNextPageToken()); } } invokeCallbackOnResult(callback, AppSearchResult.newSuccessfulResult(null)); diff --git a/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java b/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java index a1b93ce129756..830e76c622793 100644 --- a/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java +++ b/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java @@ -173,6 +173,21 @@ public final class AppSearchImpl implements Closeable { @GuardedBy("mReadWriteLock") private final Map mDocumentCountMapLocked = new ArrayMap<>(); + // Maps packages to the set of valid nextPageTokens that the package can manipulate. A token + // is unique and constant per query (i.e. the same token '123' is used to iterate through + // pages of search results). The tokens themselves are generated and tracked by + // IcingSearchEngine. IcingSearchEngine considers a token valid and won't be reused + // until we call invalidateNextPageToken on the token. + // + // Note that we synchronize on itself because the nextPageToken cache is checked at + // query-time, and queries are done in parallel with a read lock. Ideally, this would be + // guarded by the normal mReadWriteLock.writeLock, but ReentrantReadWriteLocks can't upgrade + // read to write locks. This lock should be acquired at the smallest scope possible. + // mReadWriteLock is a higher-level lock, so calls shouldn't be made out + // to any functions that grab the lock. + @GuardedBy("mNextPageTokensLocked") + private final Map> mNextPageTokensLocked = new ArrayMap<>(); + /** * The counter to check when to call {@link #checkForOptimize}. The interval is {@link * #CHECK_OPTIMIZE_INTERVAL}. @@ -837,12 +852,15 @@ public final class AppSearchImpl implements Closeable { String prefix = createPrefix(packageName, databaseName); Set allowedPrefixedSchemas = getAllowedPrefixSchemasLocked(prefix, searchSpec); - return doQueryLocked( - Collections.singleton(createPrefix(packageName, databaseName)), - allowedPrefixedSchemas, - queryExpression, - searchSpec, - sStatsBuilder); + SearchResultPage searchResultPage = + doQueryLocked( + Collections.singleton(createPrefix(packageName, databaseName)), + allowedPrefixedSchemas, + queryExpression, + searchSpec, + sStatsBuilder); + addNextPageToken(packageName, searchResultPage.getNextPageToken()); + return searchResultPage; } finally { mReadWriteLock.readLock().unlock(); if (logger != null) { @@ -956,12 +974,15 @@ public final class AppSearchImpl implements Closeable { } } - return doQueryLocked( - prefixFilters, - prefixedSchemaFilters, - queryExpression, - searchSpec, - sStatsBuilder); + SearchResultPage searchResultPage = + doQueryLocked( + prefixFilters, + prefixedSchemaFilters, + queryExpression, + searchSpec, + sStatsBuilder); + addNextPageToken(callerPackageName, searchResultPage.getNextPageToken()); + return searchResultPage; } finally { mReadWriteLock.readLock().unlock(); @@ -1093,17 +1114,20 @@ public final class AppSearchImpl implements Closeable { * *

This method belongs to query group. * + * @param packageName Package name of the caller. * @param nextPageToken The token of pre-loaded results of previously executed query. * @return The next page of results of previously executed query. - * @throws AppSearchException on IcingSearchEngine error. + * @throws AppSearchException on IcingSearchEngine error or if can't advance on nextPageToken. */ @NonNull - public SearchResultPage getNextPage(long nextPageToken) throws AppSearchException { + public SearchResultPage getNextPage(@NonNull String packageName, long nextPageToken) + throws AppSearchException { mReadWriteLock.readLock().lock(); try { throwIfClosedLocked(); mLogUtil.piiTrace("getNextPage, request", nextPageToken); + checkNextPageToken(packageName, nextPageToken); SearchResultProto searchResultProto = mIcingSearchEngineLocked.getNextPage(nextPageToken); mLogUtil.piiTrace( @@ -1122,16 +1146,26 @@ public final class AppSearchImpl implements Closeable { * *

This method belongs to query group. * + * @param packageName Package name of the caller. * @param nextPageToken The token of pre-loaded results of previously executed query to be * Invalidated. + * @throws AppSearchException if nextPageToken is unusable. */ - public void invalidateNextPageToken(long nextPageToken) { + public void invalidateNextPageToken(@NonNull String packageName, long nextPageToken) + throws AppSearchException { mReadWriteLock.readLock().lock(); try { throwIfClosedLocked(); mLogUtil.piiTrace("invalidateNextPageToken, request", nextPageToken); + checkNextPageToken(packageName, nextPageToken); mIcingSearchEngineLocked.invalidateNextPageToken(nextPageToken); + + synchronized (mNextPageTokensLocked) { + // At this point, we're guaranteed that this nextPageToken exists for this package, + // otherwise checkNextPageToken would've thrown an exception. + mNextPageTokensLocked.get(packageName).remove(nextPageToken); + } } finally { mReadWriteLock.readLock().unlock(); } @@ -1568,6 +1602,9 @@ public final class AppSearchImpl implements Closeable { Set databaseNames = entry.getValue(); if (!installedPackages.contains(packageName) && databaseNames != null) { mDocumentCountMapLocked.remove(packageName); + synchronized (mNextPageTokensLocked) { + mNextPageTokensLocked.remove(packageName); + } for (String databaseName : databaseNames) { String removedPrefix = createPrefix(packageName, databaseName); mSchemaMapLocked.remove(removedPrefix); @@ -1601,6 +1638,9 @@ public final class AppSearchImpl implements Closeable { mSchemaMapLocked.clear(); mNamespaceMapLocked.clear(); mDocumentCountMapLocked.clear(); + synchronized (mNextPageTokensLocked) { + mNextPageTokensLocked.clear(); + } if (initStatsBuilder != null) { initStatsBuilder .setHasReset(true) @@ -2015,6 +2055,32 @@ public final class AppSearchImpl implements Closeable { return schemaProto.getSchema(); } + private void addNextPageToken(String packageName, long nextPageToken) { + synchronized (mNextPageTokensLocked) { + Set tokens = mNextPageTokensLocked.get(packageName); + if (tokens == null) { + tokens = new ArraySet<>(); + mNextPageTokensLocked.put(packageName, tokens); + } + tokens.add(nextPageToken); + } + } + + private void checkNextPageToken(String packageName, long nextPageToken) + throws AppSearchException { + synchronized (mNextPageTokensLocked) { + Set nextPageTokens = mNextPageTokensLocked.get(packageName); + if (nextPageTokens == null || !nextPageTokens.contains(nextPageToken)) { + throw new AppSearchException( + AppSearchResult.RESULT_SECURITY_ERROR, + "Package \"" + + packageName + + "\" cannot use nextPageToken: " + + nextPageToken); + } + } + } + private static void addToMap( Map> map, String prefix, String prefixedValue) { Set values = map.get(prefix); diff --git a/services/tests/servicestests/src/com/android/server/appsearch/external/localstorage/AppSearchImplTest.java b/services/tests/servicestests/src/com/android/server/appsearch/external/localstorage/AppSearchImplTest.java index 91f49224fde89..5b067bc58da3a 100644 --- a/services/tests/servicestests/src/com/android/server/appsearch/external/localstorage/AppSearchImplTest.java +++ b/services/tests/servicestests/src/com/android/server/appsearch/external/localstorage/AppSearchImplTest.java @@ -868,6 +868,446 @@ public class AppSearchImplTest { assertThat(searchResultPage.getResults()).isEmpty(); } + @Test + public void testGetNextPageToken_query() throws Exception { + // Insert package1 schema + List schema1 = + ImmutableList.of(new AppSearchSchema.Builder("schema1").build()); + mAppSearchImpl.setSchema( + "package1", + "database1", + schema1, + /*visibilityStore=*/ null, + /*schemasNotDisplayedBySystem=*/ Collections.emptyList(), + /*schemasVisibleToPackages=*/ Collections.emptyMap(), + /*forceOverride=*/ false, + /*version=*/ 0); + + // Insert two package1 documents + GenericDocument document1 = + new GenericDocument.Builder<>("namespace", "id1", "schema1").build(); + GenericDocument document2 = + new GenericDocument.Builder<>("namespace", "id2", "schema1").build(); + mAppSearchImpl.putDocument("package1", "database1", document1, /*logger=*/ null); + mAppSearchImpl.putDocument("package1", "database1", document2, /*logger=*/ null); + + // Query for only 1 result per page + SearchSpec searchSpec = + new SearchSpec.Builder() + .setTermMatch(TermMatchType.Code.PREFIX_VALUE) + .setResultCountPerPage(1) + .build(); + SearchResultPage searchResultPage = + mAppSearchImpl.query("package1", "database1", "", searchSpec, /*logger=*/ null); + + // Document2 will come first because it was inserted last and default return order is + // most recent. + assertThat(searchResultPage.getResults()).hasSize(1); + assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document2); + + long nextPageToken = searchResultPage.getNextPageToken(); + searchResultPage = mAppSearchImpl.getNextPage("package1", nextPageToken); + assertThat(searchResultPage.getResults()).hasSize(1); + assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document1); + } + + @Test + public void testGetNextPageWithDifferentPackage_query() throws Exception { + // Insert package1 schema + List schema1 = + ImmutableList.of(new AppSearchSchema.Builder("schema1").build()); + mAppSearchImpl.setSchema( + "package1", + "database1", + schema1, + /*visibilityStore=*/ null, + /*schemasNotDisplayedBySystem=*/ Collections.emptyList(), + /*schemasVisibleToPackages=*/ Collections.emptyMap(), + /*forceOverride=*/ false, + /*version=*/ 0); + + // Insert two package1 documents + GenericDocument document1 = + new GenericDocument.Builder<>("namespace", "id1", "schema1").build(); + GenericDocument document2 = + new GenericDocument.Builder<>("namespace", "id2", "schema1").build(); + mAppSearchImpl.putDocument("package1", "database1", document1, /*logger=*/ null); + mAppSearchImpl.putDocument("package1", "database1", document2, /*logger=*/ null); + + // Query for only 1 result per page + SearchSpec searchSpec = + new SearchSpec.Builder() + .setTermMatch(TermMatchType.Code.PREFIX_VALUE) + .setResultCountPerPage(1) + .build(); + SearchResultPage searchResultPage = + mAppSearchImpl.query("package1", "database1", "", searchSpec, /*logger=*/ null); + + // Document2 will come first because it was inserted last and default return order is + // most recent. + assertThat(searchResultPage.getResults()).hasSize(1); + assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document2); + + long nextPageToken = searchResultPage.getNextPageToken(); + + // Try getting next page with the wrong package, package2 + AppSearchException e = + assertThrows( + AppSearchException.class, + () -> mAppSearchImpl.getNextPage("package2", nextPageToken)); + assertThat(e) + .hasMessageThat() + .contains("Package \"package2\" cannot use nextPageToken: " + nextPageToken); + assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_SECURITY_ERROR); + + // Can continue getting next page for package1 + searchResultPage = mAppSearchImpl.getNextPage("package1", nextPageToken); + assertThat(searchResultPage.getResults()).hasSize(1); + assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document1); + } + + @Test + public void testGetNextPageToken_globalQuery() throws Exception { + // Insert package1 schema + List schema1 = + ImmutableList.of(new AppSearchSchema.Builder("schema1").build()); + mAppSearchImpl.setSchema( + "package1", + "database1", + schema1, + /*visibilityStore=*/ null, + /*schemasNotDisplayedBySystem=*/ Collections.emptyList(), + /*schemasVisibleToPackages=*/ Collections.emptyMap(), + /*forceOverride=*/ false, + /*version=*/ 0); + + // Insert two package1 documents + GenericDocument document1 = + new GenericDocument.Builder<>("namespace", "id1", "schema1").build(); + GenericDocument document2 = + new GenericDocument.Builder<>("namespace", "id2", "schema1").build(); + mAppSearchImpl.putDocument("package1", "database1", document1, /*logger=*/ null); + mAppSearchImpl.putDocument("package1", "database1", document2, /*logger=*/ null); + + // Query for only 1 result per page + SearchSpec searchSpec = + new SearchSpec.Builder() + .setTermMatch(TermMatchType.Code.PREFIX_VALUE) + .setResultCountPerPage(1) + .build(); + SearchResultPage searchResultPage = + mAppSearchImpl.globalQuery( + /*queryExpression=*/ "", + searchSpec, + "package1", + /*visibilityStore=*/ null, + Process.myUid(), + /*callerHasSystemAccess=*/ false, + /*logger=*/ null); + + // Document2 will come first because it was inserted last and default return order is + // most recent. + assertThat(searchResultPage.getResults()).hasSize(1); + assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document2); + + long nextPageToken = searchResultPage.getNextPageToken(); + searchResultPage = mAppSearchImpl.getNextPage("package1", nextPageToken); + assertThat(searchResultPage.getResults()).hasSize(1); + assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document1); + } + + @Test + public void testGetNextPageWithDifferentPackage_globalQuery() throws Exception { + // Insert package1 schema + List schema1 = + ImmutableList.of(new AppSearchSchema.Builder("schema1").build()); + mAppSearchImpl.setSchema( + "package1", + "database1", + schema1, + /*visibilityStore=*/ null, + /*schemasNotDisplayedBySystem=*/ Collections.emptyList(), + /*schemasVisibleToPackages=*/ Collections.emptyMap(), + /*forceOverride=*/ false, + /*version=*/ 0); + + // Insert two package1 documents + GenericDocument document1 = + new GenericDocument.Builder<>("namespace", "id1", "schema1").build(); + GenericDocument document2 = + new GenericDocument.Builder<>("namespace", "id2", "schema1").build(); + mAppSearchImpl.putDocument("package1", "database1", document1, /*logger=*/ null); + mAppSearchImpl.putDocument("package1", "database1", document2, /*logger=*/ null); + + // Query for only 1 result per page + SearchSpec searchSpec = + new SearchSpec.Builder() + .setTermMatch(TermMatchType.Code.PREFIX_VALUE) + .setResultCountPerPage(1) + .build(); + SearchResultPage searchResultPage = + mAppSearchImpl.globalQuery( + /*queryExpression=*/ "", + searchSpec, + "package1", + /*visibilityStore=*/ null, + Process.myUid(), + /*callerHasSystemAccess=*/ false, + /*logger=*/ null); + + // Document2 will come first because it was inserted last and default return order is + // most recent. + assertThat(searchResultPage.getResults()).hasSize(1); + assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document2); + + long nextPageToken = searchResultPage.getNextPageToken(); + + // Try getting next page with the wrong package, package2 + AppSearchException e = + assertThrows( + AppSearchException.class, + () -> mAppSearchImpl.getNextPage("package2", nextPageToken)); + assertThat(e) + .hasMessageThat() + .contains("Package \"package2\" cannot use nextPageToken: " + nextPageToken); + assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_SECURITY_ERROR); + + // Can continue getting next page for package1 + searchResultPage = mAppSearchImpl.getNextPage("package1", nextPageToken); + assertThat(searchResultPage.getResults()).hasSize(1); + assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document1); + } + + @Test + public void testInvalidateNextPageToken_query() throws Exception { + // Insert package1 schema + List schema1 = + ImmutableList.of(new AppSearchSchema.Builder("schema1").build()); + mAppSearchImpl.setSchema( + "package1", + "database1", + schema1, + /*visibilityStore=*/ null, + /*schemasNotDisplayedBySystem=*/ Collections.emptyList(), + /*schemasVisibleToPackages=*/ Collections.emptyMap(), + /*forceOverride=*/ false, + /*version=*/ 0); + + // Insert two package1 documents + GenericDocument document1 = + new GenericDocument.Builder<>("namespace", "id1", "schema1").build(); + GenericDocument document2 = + new GenericDocument.Builder<>("namespace", "id2", "schema1").build(); + mAppSearchImpl.putDocument("package1", "database1", document1, /*logger=*/ null); + mAppSearchImpl.putDocument("package1", "database1", document2, /*logger=*/ null); + + // Query for only 1 result per page + SearchSpec searchSpec = + new SearchSpec.Builder() + .setTermMatch(TermMatchType.Code.PREFIX_VALUE) + .setResultCountPerPage(1) + .build(); + SearchResultPage searchResultPage = + mAppSearchImpl.query("package1", "database1", "", searchSpec, /*logger=*/ null); + + // Document2 will come first because it was inserted last and default return order is + // most recent. + assertThat(searchResultPage.getResults()).hasSize(1); + assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document2); + + long nextPageToken = searchResultPage.getNextPageToken(); + + // Invalidate the token + mAppSearchImpl.invalidateNextPageToken("package1", nextPageToken); + + // Can't get next page because we invalidated the token. + AppSearchException e = + assertThrows( + AppSearchException.class, + () -> mAppSearchImpl.getNextPage("package1", nextPageToken)); + assertThat(e) + .hasMessageThat() + .contains("Package \"package1\" cannot use nextPageToken: " + nextPageToken); + assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_SECURITY_ERROR); + } + + @Test + public void testInvalidateNextPageTokenWithDifferentPackage_query() throws Exception { + // Insert package1 schema + List schema1 = + ImmutableList.of(new AppSearchSchema.Builder("schema1").build()); + mAppSearchImpl.setSchema( + "package1", + "database1", + schema1, + /*visibilityStore=*/ null, + /*schemasNotDisplayedBySystem=*/ Collections.emptyList(), + /*schemasVisibleToPackages=*/ Collections.emptyMap(), + /*forceOverride=*/ false, + /*version=*/ 0); + + // Insert two package1 documents + GenericDocument document1 = + new GenericDocument.Builder<>("namespace", "id1", "schema1").build(); + GenericDocument document2 = + new GenericDocument.Builder<>("namespace", "id2", "schema1").build(); + mAppSearchImpl.putDocument("package1", "database1", document1, /*logger=*/ null); + mAppSearchImpl.putDocument("package1", "database1", document2, /*logger=*/ null); + + // Query for only 1 result per page + SearchSpec searchSpec = + new SearchSpec.Builder() + .setTermMatch(TermMatchType.Code.PREFIX_VALUE) + .setResultCountPerPage(1) + .build(); + SearchResultPage searchResultPage = + mAppSearchImpl.query("package1", "database1", "", searchSpec, /*logger=*/ null); + + // Document2 will come first because it was inserted last and default return order is + // most recent. + assertThat(searchResultPage.getResults()).hasSize(1); + assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document2); + + long nextPageToken = searchResultPage.getNextPageToken(); + + // Try getting next page with the wrong package, package2 + AppSearchException e = + assertThrows( + AppSearchException.class, + () -> mAppSearchImpl.invalidateNextPageToken("package2", nextPageToken)); + assertThat(e) + .hasMessageThat() + .contains("Package \"package2\" cannot use nextPageToken: " + nextPageToken); + assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_SECURITY_ERROR); + + // Can continue getting next page for package1 + searchResultPage = mAppSearchImpl.getNextPage("package1", nextPageToken); + assertThat(searchResultPage.getResults()).hasSize(1); + assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document1); + } + + @Test + public void testInvalidateNextPageToken_globalQuery() throws Exception { + // Insert package1 schema + List schema1 = + ImmutableList.of(new AppSearchSchema.Builder("schema1").build()); + mAppSearchImpl.setSchema( + "package1", + "database1", + schema1, + /*visibilityStore=*/ null, + /*schemasNotDisplayedBySystem=*/ Collections.emptyList(), + /*schemasVisibleToPackages=*/ Collections.emptyMap(), + /*forceOverride=*/ false, + /*version=*/ 0); + + // Insert two package1 documents + GenericDocument document1 = + new GenericDocument.Builder<>("namespace", "id1", "schema1").build(); + GenericDocument document2 = + new GenericDocument.Builder<>("namespace", "id2", "schema1").build(); + mAppSearchImpl.putDocument("package1", "database1", document1, /*logger=*/ null); + mAppSearchImpl.putDocument("package1", "database1", document2, /*logger=*/ null); + + // Query for only 1 result per page + SearchSpec searchSpec = + new SearchSpec.Builder() + .setTermMatch(TermMatchType.Code.PREFIX_VALUE) + .setResultCountPerPage(1) + .build(); + SearchResultPage searchResultPage = + mAppSearchImpl.globalQuery( + /*queryExpression=*/ "", + searchSpec, + "package1", + /*visibilityStore=*/ null, + Process.myUid(), + /*callerHasSystemAccess=*/ false, + /*logger=*/ null); + + // Document2 will come first because it was inserted last and default return order is + // most recent. + assertThat(searchResultPage.getResults()).hasSize(1); + assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document2); + + long nextPageToken = searchResultPage.getNextPageToken(); + + // Invalidate the token + mAppSearchImpl.invalidateNextPageToken("package1", nextPageToken); + + // Can't get next page because we invalidated the token. + AppSearchException e = + assertThrows( + AppSearchException.class, + () -> mAppSearchImpl.getNextPage("package1", nextPageToken)); + assertThat(e) + .hasMessageThat() + .contains("Package \"package1\" cannot use nextPageToken: " + nextPageToken); + assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_SECURITY_ERROR); + } + + @Test + public void testInvalidateNextPageTokenWithDifferentPackage_globalQuery() throws Exception { + // Insert package1 schema + List schema1 = + ImmutableList.of(new AppSearchSchema.Builder("schema1").build()); + mAppSearchImpl.setSchema( + "package1", + "database1", + schema1, + /*visibilityStore=*/ null, + /*schemasNotDisplayedBySystem=*/ Collections.emptyList(), + /*schemasVisibleToPackages=*/ Collections.emptyMap(), + /*forceOverride=*/ false, + /*version=*/ 0); + + // Insert two package1 documents + GenericDocument document1 = + new GenericDocument.Builder<>("namespace", "id1", "schema1").build(); + GenericDocument document2 = + new GenericDocument.Builder<>("namespace", "id2", "schema1").build(); + mAppSearchImpl.putDocument("package1", "database1", document1, /*logger=*/ null); + mAppSearchImpl.putDocument("package1", "database1", document2, /*logger=*/ null); + + // Query for only 1 result per page + SearchSpec searchSpec = + new SearchSpec.Builder() + .setTermMatch(TermMatchType.Code.PREFIX_VALUE) + .setResultCountPerPage(1) + .build(); + SearchResultPage searchResultPage = + mAppSearchImpl.globalQuery( + /*queryExpression=*/ "", + searchSpec, + "package1", + /*visibilityStore=*/ null, + Process.myUid(), + /*callerHasSystemAccess=*/ false, + /*logger=*/ null); + + // Document2 will come first because it was inserted last and default return order is + // most recent. + assertThat(searchResultPage.getResults()).hasSize(1); + assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document2); + + long nextPageToken = searchResultPage.getNextPageToken(); + + // Try getting next page with the wrong package, package2 + AppSearchException e = + assertThrows( + AppSearchException.class, + () -> mAppSearchImpl.invalidateNextPageToken("package2", nextPageToken)); + assertThat(e) + .hasMessageThat() + .contains("Package \"package2\" cannot use nextPageToken: " + nextPageToken); + assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_SECURITY_ERROR); + + // Can continue getting next page for package1 + searchResultPage = mAppSearchImpl.getNextPage("package1", nextPageToken); + assertThat(searchResultPage.getResults()).hasSize(1); + assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document1); + } + @Test public void testRemoveEmptyDatabase_noExceptionThrown() throws Exception { SearchSpec searchSpec = @@ -1777,11 +2217,11 @@ public class AppSearchImplTest { assertThrows( IllegalStateException.class, - () -> appSearchImpl.getNextPage(/*nextPageToken=*/ 1L)); + () -> appSearchImpl.getNextPage("package", /*nextPageToken=*/ 1L)); assertThrows( IllegalStateException.class, - () -> appSearchImpl.invalidateNextPageToken(/*nextPageToken=*/ 1L)); + () -> appSearchImpl.invalidateNextPageToken("package", /*nextPageToken=*/ 1L)); assertThrows( IllegalStateException.class,