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 package android.app.appsearch.cts.app;
17 
18 import static android.Manifest.permission.READ_CALENDAR;
19 import static android.Manifest.permission.READ_CONTACTS;
20 import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
21 import static android.Manifest.permission.READ_GLOBAL_APP_SEARCH_DATA;
22 import static android.Manifest.permission.READ_SMS;
23 import static android.app.appsearch.testutil.AppSearchTestUtils.calculateDigest;
24 import static android.app.appsearch.testutil.AppSearchTestUtils.checkIsBatchResultSuccess;
25 import static android.app.appsearch.testutil.AppSearchTestUtils.generateRandomBytes;
26 
27 import static com.google.common.truth.Truth.assertThat;
28 
29 import static org.junit.Assert.assertTrue;
30 import static org.junit.Assert.assertFalse;
31 
32 import android.annotation.NonNull;
33 import android.app.appsearch.AppSearchBatchResult;
34 import android.app.appsearch.AppSearchBlobHandle;
35 import android.app.appsearch.AppSearchResult;
36 import android.app.appsearch.AppSearchSchema;
37 import android.app.appsearch.AppSearchSchema.PropertyConfig;
38 import android.app.appsearch.AppSearchSchema.StringPropertyConfig;
39 import android.app.appsearch.AppSearchSessionShim;
40 import android.app.appsearch.GenericDocument;
41 import android.app.appsearch.GetByDocumentIdRequest;
42 import android.app.appsearch.GetSchemaResponse;
43 import android.app.appsearch.GlobalSearchSessionShim;
44 import android.app.appsearch.JoinSpec;
45 import android.app.appsearch.OpenBlobForReadResponse;
46 import android.app.appsearch.PackageIdentifier;
47 import android.app.appsearch.PutDocumentsRequest;
48 import android.app.appsearch.ReportSystemUsageRequest;
49 import android.app.appsearch.ReportUsageRequest;
50 import android.app.appsearch.SearchResult;
51 import android.app.appsearch.SearchResultsShim;
52 import android.app.appsearch.SearchSpec;
53 import android.app.appsearch.SetSchemaRequest;
54 import android.app.appsearch.observer.DocumentChangeInfo;
55 import android.app.appsearch.observer.ObserverSpec;
56 import android.app.appsearch.testutil.AppSearchEmail;
57 import android.app.appsearch.testutil.AppSearchTestUtils;
58 import android.app.appsearch.testutil.PackageUtil;
59 import android.app.appsearch.testutil.SystemUtil;
60 import android.app.appsearch.testutil.TestObserverCallback;
61 import android.app.appsearch.util.DocumentIdUtil;
62 import android.content.ComponentName;
63 import android.content.Context;
64 import android.content.Intent;
65 import android.content.ServiceConnection;
66 import android.os.Bundle;
67 import android.os.IBinder;
68 import android.os.ParcelFileDescriptor;
69 import android.platform.test.annotations.RequiresFlagsEnabled;
70 import android.util.Log;
71 
72 import androidx.test.core.app.ApplicationProvider;
73 import androidx.test.filters.SdkSuppress;
74 
75 import com.android.appsearch.flags.Flags;
76 import com.android.cts.appsearch.ICommandReceiver;
77 
78 import com.google.common.collect.ImmutableList;
79 import com.google.common.collect.ImmutableSet;
80 import com.google.common.io.BaseEncoding;
81 import com.google.common.util.concurrent.MoreExecutors;
82 
83 import org.junit.After;
84 import org.junit.Before;
85 import org.junit.Rule;
86 import org.junit.Test;
87 import org.junit.rules.RuleChain;
88 import org.junit.runner.RunWith;
89 import org.junit.runners.JUnit4;
90 
91 import java.io.InputStream;
92 import java.util.ArrayList;
93 import java.util.Collections;
94 import java.util.List;
95 import java.util.Set;
96 import java.util.concurrent.BlockingQueue;
97 import java.util.concurrent.Executor;
98 import java.util.concurrent.LinkedBlockingQueue;
99 import java.util.concurrent.TimeUnit;
100 import java.util.concurrent.atomic.AtomicReference;
101 import java.util.stream.Collectors;
102 
103 /**
104  * This class can be extended by tests in platforms like Android Framework and GMSCore which host an
105  * AppSearch service but can't be run in non-platform environments like JetPack.
106  */
107 @RunWith(JUnit4.class)
108 public abstract class GlobalSearchSessionServiceCtsTestBase {
109 
110     @Rule public final RuleChain mRuleChain = AppSearchTestUtils.createCommonTestRules();
111 
112     private static final long TIMEOUT_BIND_SERVICE_SEC = 10;
113 
114     private static final String TAG = "GlobalSearchSessionServiceCtsTestBase";
115 
116     private static final String PKG_A = "com.android.cts.appsearch.helper.a";
117 
118     // To generate, run `apksigner` on the build APK. e.g.
119     //   ./apksigner verify --print-certs \
120     //   ~/sc-dev/out/soong/.intermediates/cts/tests/appsearch/CtsAppSearchTestHelperA/\
121     //   android_common/CtsAppSearchTestHelperA.apk`
122     // to get the SHA-256 digest. All characters need to be uppercase.
123     //
124     // Note: May need to switch the "sdk_version" of the test app from "test_current" to "30" before
125     // building the apk and running apksigner
126     private static final byte[] PKG_A_CERT_SHA256 =
127             BaseEncoding.base16()
128                     .decode("A90B80BD307B71BB4029674C5C4FE18066994E352EAC933B7B68266210CAFB53");
129 
130     private static final String PKG_B = "com.android.cts.appsearch.helper.b";
131 
132     // To generate, run `apksigner` on the build APK. e.g.
133     //   ./apksigner verify --print-certs \
134     //   ~/sc-dev/out/soong/.intermediates/cts/tests/appsearch/CtsAppSearchTestHelperB/\
135     //   android_common/CtsAppSearchTestHelperB.apk`
136     // to get the SHA-256 digest. All characters need to be uppercase.
137     //
138     // Note: May need to switch the "sdk_version" of the test app from "test_current" to "30" before
139     // building the apk and running apksigner
140     private static final byte[] PKG_B_CERT_SHA256 =
141             BaseEncoding.base16()
142                     .decode("88C0B41A31943D13226C3F22A86A6B4F300315575A6BC533CBF16C4EF3CFAA37");
143 
144     private static final String HELPER_SERVICE =
145             "com.android.cts.appsearch.helper.AppSearchTestService";
146 
147     private static final String TEXT = "foo";
148 
149     private static final String NAMESPACE_NAME = "namespace";
150 
151     private static final AppSearchEmail EMAIL_DOCUMENT =
152             new AppSearchEmail.Builder(NAMESPACE_NAME, "id1")
153                     .setFrom("[email protected]")
154                     .setTo("[email protected]", "[email protected]")
155                     .setSubject(TEXT)
156                     .setBody("this is the body of the email")
157                     .build();
158 
159     private static final String DB_NAME = "database";
160 
161 
162     private GlobalSearchSessionShim mGlobalSearchSession;
163     private AppSearchSessionShim mDb;
164 
165     private Context mContext;
166 
167     @Before
setUp()168     public void setUp() throws Exception {
169         mContext = ApplicationProvider.getApplicationContext();
170         mDb = createSearchSessionAsync(DB_NAME);
171         cleanup();
172     }
173 
174     @After
tearDown()175     public void tearDown() throws Exception {
176         // Cleanup whatever documents may still exist in these databases.
177         cleanup();
178     }
179 
createSearchSessionAsync( @onNull String dbName)180     protected abstract AppSearchSessionShim createSearchSessionAsync(
181             @NonNull String dbName) throws Exception;
182 
createGlobalSearchSessionAsync( @onNull Context context)183     protected abstract GlobalSearchSessionShim createGlobalSearchSessionAsync(
184             @NonNull Context context) throws Exception;
185 
cleanup()186     private void cleanup() throws Exception {
187         mDb.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
188 
189         clearData(PKG_A, DB_NAME);
190         clearData(PKG_B, DB_NAME);
191     }
192 
193     // We could add a third package, PKG_C, that the cts package cannot query. However, this does
194     // not allow us to bind to PKG_C, meaning we can't index documents in PKG_C, making it useless.
195     // Instead, we will take advantage of the fact that the cts package can query both PKG_A and
196     // PKG_B, while PKG_A and PKG_B cannot query each other. We'll focus on the fact that PKG_A
197     // cannot query PKG_B, but can query the cts package.
198     //
199     // So we'll index two schemas, one with a publiclyVisibleTargetPackage of the cts package, and
200     // another with a publiclyVisibleTargetPackage of PKG_B. We can't index these in PKG_A, as
201     // packages always have visibility to schemas they hold. We'll index them both in PKG_B,
202     // ensuring that the "cts schema" is still visible, as well as the cts package (this one),
203     // ensuring that the "PKG_B schema" is not visible.
204     //
205     // In summary:
206     // Doc in accessible package, accessible publiclyVisibleTargetPackage: searchable
207     // Doc in inaccessible package, accessible publiclyVisibleTargetPackage: searchable
208     // Doc in accessible package, inaccessible publiclyVisibleTargetPackage: not searchable
209     // Doc in inaccessible package, inaccessible publiclyVisibleTargetPackage: not searchable
210     @SdkSuppress(minSdkVersion = 34)
211     // TODO(b/275592563): Figure out why canPackageQuery works different on T
212     @Test
testPublicVisibility_accessibleTestPackage()213     public void testPublicVisibility_accessibleTestPackage() throws Exception {
214         // Ensure manifest files are set up correctly.
215         String ctsPackageName = mContext.getPackageName();
216         String ctsSchemaName = ctsPackageName + "Schema";
217 
218         assertThat(mContext.getPackageManager().canPackageQuery(PKG_A, ctsPackageName)).isTrue();
219         assertThat(mContext.getPackageManager().canPackageQuery(PKG_A, PKG_B)).isFalse();
220 
221         AppSearchSchema ctsSchema = new AppSearchSchema.Builder(ctsSchemaName)
222                 .addProperty(new StringPropertyConfig.Builder("searchable")
223                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
224                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
225                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES).build())
226                 .build();
227         // pkg b schema
228         AppSearchSchema bSchema = new AppSearchSchema.Builder("BSchema")
229                 .addProperty(new StringPropertyConfig.Builder("searchable")
230                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
231                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
232                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES).build())
233                 .build();
234 
235         // Index schemas in the cts package
236         mDb.setSchemaAsync(new SetSchemaRequest.Builder()
237                 .addSchemas(ctsSchema, bSchema)
238                 .setPubliclyVisibleSchema(ctsSchema.getSchemaType(),
239                         new PackageIdentifier(ctsPackageName,
240                                 PackageUtil.getSelfPackageSha256Cert(mContext)))
241                 .setPubliclyVisibleSchema(bSchema.getSchemaType(),
242                         new PackageIdentifier(PKG_B, PKG_B_CERT_SHA256))
243                 .build()).get();
244         GenericDocument ctsDoc =
245                 new GenericDocument.Builder<>(NAMESPACE_NAME, "id1", ctsSchemaName)
246                         .setPropertyString("searchable", "pineapple from com.android.cts.appsearch")
247                         .build();
248         GenericDocument bDoc =
249                 new GenericDocument.Builder<>(NAMESPACE_NAME, "id2", "BSchema")
250                         .setPropertyString("searchable",
251                                 "pineapple from com.android.cts.appserach.helper.b")
252                         .build();
253         checkIsBatchResultSuccess(mDb.putAsync(new PutDocumentsRequest.Builder()
254                 .addGenericDocuments(ctsDoc, bDoc).build()));
255 
256         // Assert that this package can search for 2
257         SearchSpec searchSpec = new SearchSpec.Builder()
258                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
259                 .build();
260 
261         SearchResultsShim results = mDb.search("pineapple", searchSpec);
262         List<SearchResult> page = results.getNextPageAsync().get();
263         assertThat(page).hasSize(2);
264 
265         // Assert PKG_A can see cts
266         GlobalSearchSessionServiceCtsTestBase.TestServiceConnection serviceConnection =
267                 bindToHelperService(PKG_A);
268         try {
269             ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
270             List<String> packageBResults = commandReceiver.globalSearch("pineapple");
271             // Only the cts doc
272             assertThat(packageBResults).hasSize(1);
273             assertThat(packageBResults).containsExactly(ctsDoc.toString());
274         } finally {
275             serviceConnection.unbind();
276         }
277     }
278 
279     @SdkSuppress(minSdkVersion = 34)
280     // TODO(b/275592563): Figure out why canPackageQuery works different on T
281     @Test
testPublicVisibility_inaccessibleHelperApp()282     public void testPublicVisibility_inaccessibleHelperApp() throws Exception {
283         String ctsPackageName = mContext.getPackageName();
284         String ctsSchemaName = ctsPackageName + "Schema";
285         GenericDocument ctsDoc =
286                 new GenericDocument.Builder<>(NAMESPACE_NAME, "id1", ctsSchemaName)
287                         .setPropertyString("searchable", "pineapple from " + ctsPackageName)
288                         .build();
289 
290         assertThat(mContext.getPackageManager().canPackageQuery(PKG_A, ctsPackageName)).isTrue();
291         assertThat(mContext.getPackageManager().canPackageQuery(PKG_A, PKG_B)).isFalse();
292 
293         // Index schemas in the B package
294         GlobalSearchSessionServiceCtsTestBase.TestServiceConnection serviceConnection =
295                 bindToHelperService(PKG_B);
296         try {
297             ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
298             boolean documentVisibilitySetup =
299                     commandReceiver.setUpPubliclyVisibleDocuments(
300                             ctsPackageName, PackageUtil.getSelfPackageSha256Cert(mContext), PKG_B,
301                             PKG_B_CERT_SHA256);
302             assertThat(documentVisibilitySetup).isTrue();
303 
304             // Assert that B sees two documents
305             List<String> results = commandReceiver.globalSearch("pineapple");
306             assertThat(results).hasSize(2);
307         } finally {
308             serviceConnection.unbind();
309         }
310 
311         // Assert that A just sees the cts document
312         serviceConnection = bindToHelperService(PKG_A);
313         try {
314             ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
315             List<String> results = commandReceiver.globalSearch("pineapple");
316             // Only the cts doc
317             assertThat(results).hasSize(1);
318             String resultWithoutTimestamp =
319                     results.get(0).replaceAll("creationTimestampMillis: [0-9]+",
320                             "creationTimestampMillis: " + ctsDoc.getCreationTimestampMillis());
321 
322             assertThat(resultWithoutTimestamp).isEqualTo(ctsDoc.toString());
323 
324         } finally {
325             serviceConnection.unbind();
326         }
327     }
328 
329     @SdkSuppress(minSdkVersion = 34)
330     // TODO(b/275592563): Figure out why canPackageQuery works different on T
331     @Test
testPublicVisibility_invalidCertificate()332     public void testPublicVisibility_invalidCertificate() throws Exception {
333         // Ensure manifest files are set up correctly.
334         String ctsPackageName = mContext.getPackageName();
335         String ctsSchemaName = ctsPackageName + "Schema";
336 
337         // This test will index a publicly accessible document in both PKG_B and the cts package,
338         // but configured with an invalid sha 256 cert. Both should be inaccessible. We can also
339         // check that PKG_A can't access publicly visible documents with the publicly visible target
340         // package set to PKG_A itself if the certificate doesn't match.
341 
342         assertThat(mContext.getPackageManager().canPackageQuery(PKG_A, ctsPackageName)).isTrue();
343         assertThat(mContext.getPackageManager().canPackageQuery(PKG_A, PKG_B)).isFalse();
344 
345         AppSearchSchema ctsSchema = new AppSearchSchema.Builder(ctsSchemaName)
346                 .addProperty(new StringPropertyConfig.Builder("searchable")
347                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
348                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
349                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES).build())
350                 .build();
351         // pkg a schema
352         AppSearchSchema aSchema = new AppSearchSchema.Builder("ASchema")
353                 .addProperty(new StringPropertyConfig.Builder("searchable")
354                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
355                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
356                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES).build())
357                 .build();
358 
359         byte[] invalidCert = new byte[32];
360         invalidCert[16] = 48;
361 
362         // Index schemas in the cts package
363         mDb.setSchemaAsync(new SetSchemaRequest.Builder()
364                 .addSchemas(ctsSchema, aSchema)
365                 .setPubliclyVisibleSchema(ctsSchema.getSchemaType(),
366                         new PackageIdentifier(ctsPackageName, invalidCert))
367                 .setPubliclyVisibleSchema(aSchema.getSchemaType(),
368                         new PackageIdentifier(PKG_A, invalidCert))
369                 .build()).get();
370         GenericDocument ctsDoc =
371                 new GenericDocument.Builder<>(NAMESPACE_NAME, "id1", ctsSchemaName)
372                         .setPropertyString("searchable", "pineapple from com.android.cts.appsearch")
373                         .build();
374         GenericDocument aDoc =
375                 new GenericDocument.Builder<>(NAMESPACE_NAME, "id2", "ASchema")
376                         .setPropertyString("searchable",
377                                 "pineapple from com.android.cts.appserach.helper.a")
378                         .build();
379         checkIsBatchResultSuccess(mDb.putAsync(new PutDocumentsRequest.Builder()
380                 .addGenericDocuments(ctsDoc, aDoc).build()));
381 
382         // Check that this package can search for 2
383         SearchSpec searchSpec = new SearchSpec.Builder()
384                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
385                 .build();
386         SearchResultsShim results = mDb.search("pineapple", searchSpec);
387         List<SearchResult> page = results.getNextPageAsync().get();
388         assertThat(page).hasSize(2);
389 
390         // Assert PKG_A can't see the documents
391         GlobalSearchSessionServiceCtsTestBase.TestServiceConnection serviceConnection =
392                 bindToHelperService(PKG_A);
393         try {
394             ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
395             List<String> ctsPackageResults = commandReceiver.globalSearch("pineapple");
396             assertThat(ctsPackageResults).isEmpty();
397         } finally {
398             serviceConnection.unbind();
399         }
400 
401         // Index schemas in the B package
402         serviceConnection = bindToHelperService(PKG_B);
403         try {
404             ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
405             boolean documentVisibilitySetup = commandReceiver.setUpPubliclyVisibleDocuments(
406                     ctsPackageName, invalidCert, PKG_A, invalidCert);
407             assertThat(documentVisibilitySetup).isTrue();
408 
409             // Assert that B sees two documents
410             List<String> bResults = commandReceiver.globalSearch("pineapple");
411             assertThat(bResults).hasSize(2);
412         } finally {
413             serviceConnection.unbind();
414         }
415 
416         // Assert that A just sees the cts document
417         serviceConnection = bindToHelperService(PKG_A);
418         try {
419             ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
420             List<String> aResults = commandReceiver.globalSearch("pineapple");
421             assertThat(aResults).isEmpty();
422         } finally {
423             serviceConnection.unbind();
424         }
425     }
426 
427     @Test
testNoPackageAccess_default()428     public void testNoPackageAccess_default() throws Exception {
429         mDb.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
430                 .get();
431         checkIsBatchResultSuccess(
432                 mDb.putAsync(
433                         new PutDocumentsRequest.Builder()
434                                 .addGenericDocuments(EMAIL_DOCUMENT)
435                                 .build()));
436 
437         // No package has access by default
438         assertPackageCannotAccess(PKG_A);
439         assertPackageCannotAccess(PKG_B);
440     }
441 
442     @Test
testNoPackageAccess_wrongPackageName()443     public void testNoPackageAccess_wrongPackageName() throws Exception {
444         mDb.setSchemaAsync(
445                         new SetSchemaRequest.Builder()
446                                 .addSchemas(AppSearchEmail.SCHEMA)
447                                 .setSchemaTypeVisibilityForPackage(
448                                         AppSearchEmail.SCHEMA_TYPE,
449                                         /*visible=*/ true,
450                                         new PackageIdentifier(
451                                                 "some.other.package", PKG_A_CERT_SHA256))
452                                 .build())
453                 .get();
454         checkIsBatchResultSuccess(
455                 mDb.putAsync(
456                         new PutDocumentsRequest.Builder()
457                                 .addGenericDocuments(EMAIL_DOCUMENT)
458                                 .build()));
459 
460         assertPackageCannotAccess(PKG_A);
461     }
462 
463     @Test
testNoPackageAccess_wrongCertificate()464     public void testNoPackageAccess_wrongCertificate() throws Exception {
465         mDb.setSchemaAsync(
466                         new SetSchemaRequest.Builder()
467                                 .addSchemas(AppSearchEmail.SCHEMA)
468                                 .setSchemaTypeVisibilityForPackage(
469                                         AppSearchEmail.SCHEMA_TYPE,
470                                         /*visible=*/ true,
471                                         new PackageIdentifier(PKG_A, new byte[] {10}))
472                                 .build())
473                 .get();
474         checkIsBatchResultSuccess(
475                 mDb.putAsync(
476                         new PutDocumentsRequest.Builder()
477                                 .addGenericDocuments(EMAIL_DOCUMENT)
478                                 .build()));
479 
480         assertPackageCannotAccess(PKG_A);
481     }
482 
483     @Test
testAllowPackageAccess()484     public void testAllowPackageAccess() throws Exception {
485         mDb.setSchemaAsync(
486                         new SetSchemaRequest.Builder()
487                                 .addSchemas(AppSearchEmail.SCHEMA)
488                                 .setSchemaTypeVisibilityForPackage(
489                                         AppSearchEmail.SCHEMA_TYPE,
490                                         /*visible=*/ true,
491                                         new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
492                                 .build())
493                 .get();
494         checkIsBatchResultSuccess(
495                 mDb.putAsync(
496                         new PutDocumentsRequest.Builder()
497                                 .addGenericDocuments(EMAIL_DOCUMENT)
498                                 .build()));
499 
500         assertPackageCanAccess(EMAIL_DOCUMENT, PKG_A);
501         assertPackageCannotAccess(PKG_B);
502     }
503 
504     @SdkSuppress(minSdkVersion = 34)
505     @Test
testAllowPackageAccess_polymorphism()506     public void testAllowPackageAccess_polymorphism() throws Exception {
507         // Create two types, Person and Artist, where Artist is a child type of Person.
508         AppSearchSchema personSchema =
509                 new AppSearchSchema.Builder("Person")
510                         .addProperty(
511                                 new StringPropertyConfig.Builder("name")
512                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
513                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
514                                         .setIndexingType(
515                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
516                                         .build())
517                         .build();
518         AppSearchSchema artistSchema =
519                 new AppSearchSchema.Builder("Artist")
520                         .addParentType("Person")
521                         .addProperty(
522                                 new StringPropertyConfig.Builder("name")
523                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
524                                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
525                                         .setIndexingType(
526                                                 StringPropertyConfig.INDEXING_TYPE_PREFIXES)
527                                         .build())
528                         .build();
529 
530         // Allow PKG_A to access Person, but not allow to access Artist.
531         mDb.setSchemaAsync(
532                         new SetSchemaRequest.Builder()
533                                 .addSchemas(personSchema)
534                                 .addSchemas(artistSchema)
535                                 .setSchemaTypeVisibilityForPackage(
536                                         "Person",
537                                         /*visible=*/ true,
538                                         new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
539                                 .setSchemaTypeVisibilityForPackage(
540                                         "Artist",
541                                         /*visible=*/ false,
542                                         new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
543                                 .build())
544                 .get();
545 
546         // Index a person document and an artist document
547         GenericDocument personDoc =
548                 new GenericDocument.Builder<>(NAMESPACE_NAME, "id1", "Person")
549                         .setCreationTimestampMillis(1000)
550                         .setPropertyString("name", TEXT)
551                         .build();
552         GenericDocument artistDoc =
553                 new GenericDocument.Builder<>(NAMESPACE_NAME, "id2", "Artist")
554                         .setCreationTimestampMillis(1000)
555                         .setPropertyString("name", TEXT)
556                         .build();
557         checkIsBatchResultSuccess(
558                 mDb.putAsync(
559                         new PutDocumentsRequest.Builder()
560                                 .addGenericDocuments(personDoc)
561                                 .addGenericDocuments(artistDoc)
562                                 .build()));
563 
564         // Check that PKG_A can only access personDoc
565         assertPackageCanAccess(List.of(personDoc), PKG_A);
566         assertPackageCannotAccess(PKG_B);
567     }
568 
569     @Test
testRemoveVisibilitySetting_noRemainingSettings()570     public void testRemoveVisibilitySetting_noRemainingSettings() throws Exception {
571         // Set schema and allow PKG_A to access.
572         mDb.setSchemaAsync(
573                 new SetSchemaRequest.Builder()
574                         .addSchemas(AppSearchEmail.SCHEMA)
575                         .setSchemaTypeVisibilityForPackage(
576                                 AppSearchEmail.SCHEMA_TYPE,
577                                 /*visible=*/ true,
578                                 new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
579                         .build())
580                 .get();
581         checkIsBatchResultSuccess(
582                 mDb.putAsync(
583                         new PutDocumentsRequest.Builder()
584                                 .addGenericDocuments(EMAIL_DOCUMENT)
585                                 .build()));
586 
587         // PKG_A can access.
588         assertPackageCanAccess(EMAIL_DOCUMENT, PKG_A);
589         assertPackageCannotAccess(PKG_B);
590 
591         // Remove the schema.
592         mDb.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
593 
594         // Add the schema back with default visibility setting.
595         mDb.setSchemaAsync(new SetSchemaRequest.Builder()
596                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
597 
598         // No package can access.
599         assertPackageCannotAccess(PKG_A);
600         assertPackageCannotAccess(PKG_B);
601     }
602 
603     @Test
testAllowMultiplePackageAccess()604     public void testAllowMultiplePackageAccess() throws Exception {
605         mDb.setSchemaAsync(
606                         new SetSchemaRequest.Builder()
607                                 .addSchemas(AppSearchEmail.SCHEMA)
608                                 .setSchemaTypeVisibilityForPackage(
609                                         AppSearchEmail.SCHEMA_TYPE,
610                                         /*visible=*/ true,
611                                         new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
612                                 .setSchemaTypeVisibilityForPackage(
613                                         AppSearchEmail.SCHEMA_TYPE,
614                                         /*visible=*/ true,
615                                         new PackageIdentifier(PKG_B, PKG_B_CERT_SHA256))
616                                 .build())
617                 .get();
618         checkIsBatchResultSuccess(
619                 mDb.putAsync(
620                         new PutDocumentsRequest.Builder()
621                                 .addGenericDocuments(EMAIL_DOCUMENT)
622                                 .build()));
623 
624         assertPackageCanAccess(EMAIL_DOCUMENT, PKG_A);
625         assertPackageCanAccess(EMAIL_DOCUMENT, PKG_B);
626     }
627 
628     @Test
testNoPackageAccess_revoked()629     public void testNoPackageAccess_revoked() throws Exception {
630         mDb.setSchemaAsync(
631                         new SetSchemaRequest.Builder()
632                                 .addSchemas(AppSearchEmail.SCHEMA)
633                                 .setSchemaTypeVisibilityForPackage(
634                                         AppSearchEmail.SCHEMA_TYPE,
635                                         /*visible=*/ true,
636                                         new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
637                                 .build())
638                 .get();
639         checkIsBatchResultSuccess(
640                 mDb.putAsync(
641                         new PutDocumentsRequest.Builder()
642                                 .addGenericDocuments(EMAIL_DOCUMENT)
643                                 .build()));
644         assertPackageCanAccess(EMAIL_DOCUMENT, PKG_A);
645 
646         // Set the schema again, but package access as false.
647         mDb.setSchemaAsync(
648                         new SetSchemaRequest.Builder()
649                                 .addSchemas(AppSearchEmail.SCHEMA)
650                                 .setSchemaTypeVisibilityForPackage(
651                                         AppSearchEmail.SCHEMA_TYPE,
652                                         /*visible=*/ false,
653                                         new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
654                                 .build())
655                 .get();
656         checkIsBatchResultSuccess(
657                 mDb.putAsync(
658                         new PutDocumentsRequest.Builder()
659                                 .addGenericDocuments(EMAIL_DOCUMENT)
660                                 .build()));
661         assertPackageCannotAccess(PKG_A);
662 
663         // Set the schema again, but with default (i.e. no) access
664         mDb.setSchemaAsync(
665                         new SetSchemaRequest.Builder()
666                                 .addSchemas(AppSearchEmail.SCHEMA)
667                                 .setSchemaTypeVisibilityForPackage(
668                                         AppSearchEmail.SCHEMA_TYPE,
669                                         /*visible=*/ false,
670                                         new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
671                                 .build())
672                 .get();
673         checkIsBatchResultSuccess(
674                 mDb.putAsync(
675                         new PutDocumentsRequest.Builder()
676                                 .addGenericDocuments(EMAIL_DOCUMENT)
677                                 .build()));
678         assertPackageCannotAccess(PKG_A);
679     }
680 
681     @Test
testAllowPermissionAccess()682     public void testAllowPermissionAccess() throws Exception {
683         // index a global searchable document in pkg_A and set it needs READ_SMS to read it.
684         indexGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1",
685                 ImmutableSet.of(ImmutableSet.of(SetSchemaRequest.READ_SMS)));
686 
687         SystemUtil.runWithShellPermissionIdentity(
688                 () -> {
689                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
690 
691                     // Can get the document
692                     AppSearchBatchResult<String, GenericDocument> result = mGlobalSearchSession
693                             .getByDocumentIdAsync(PKG_A, "database",
694                                     new GetByDocumentIdRequest.Builder("namespace")
695                                             .addIds("id1")
696                                             .build()).get();
697                     assertThat(result.getSuccesses()).hasSize(1);
698                 },
699                 READ_SMS);
700     }
701 
702     //TODO(b/202194495) add test for READ_HOME_APP_SEARCH_DATA and READ_ASSISTANT_APP_SEARCH_DATA
703     // once they are available in Shell.
704     @Test
testRequireAllPermissionsOfAnyCombinationToAccess()705     public void testRequireAllPermissionsOfAnyCombinationToAccess() throws Exception {
706         // index a global searchable document in pkg_A and set it needs both READ_SMS and
707         // READ_CALENDAR or READ_HOME_APP_SEARCH_DATA only or READ_ASSISTANT_APP_SEARCH_DATA
708         // only to read it.
709         indexGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1",
710                 ImmutableSet.of(
711                         ImmutableSet.of(SetSchemaRequest.READ_SMS,
712                                 SetSchemaRequest.READ_CALENDAR),
713                         ImmutableSet.of(SetSchemaRequest.READ_CONTACTS),
714                         ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)));
715 
716         // Has READ_SMS only cannot access the document.
717         SystemUtil.runWithShellPermissionIdentity(
718                 () -> {
719                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
720 
721                     // Can't get the document
722                     AppSearchBatchResult<String, GenericDocument> result = mGlobalSearchSession
723                             .getByDocumentIdAsync(PKG_A, "database",
724                                     new GetByDocumentIdRequest.Builder("namespace")
725                                             .addIds("id1")
726                                             .build()).get();
727                     assertThat(result.getSuccesses()).isEmpty();
728                 },
729                 READ_SMS);
730 
731         // Has READ_SMS and READ_CALENDAR can access the document.
732         SystemUtil.runWithShellPermissionIdentity(
733                 () -> {
734                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
735 
736                     // Can get the document
737                     AppSearchBatchResult<String, GenericDocument> result = mGlobalSearchSession
738                             .getByDocumentIdAsync(PKG_A, "database",
739                                     new GetByDocumentIdRequest.Builder("namespace")
740                                             .addIds("id1")
741                                             .build()).get();
742                     assertThat(result.getSuccesses()).hasSize(1);
743                 },
744                 READ_SMS, READ_CALENDAR);
745 
746         // Has READ_CONTACTS can access the document also.
747         SystemUtil.runWithShellPermissionIdentity(
748                 () -> {
749                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
750 
751                     // Can get the document
752                     AppSearchBatchResult<String, GenericDocument> result = mGlobalSearchSession
753                             .getByDocumentIdAsync(PKG_A, "database",
754                                     new GetByDocumentIdRequest.Builder("namespace")
755                                             .addIds("id1")
756                                             .build()).get();
757                     assertThat(result.getSuccesses()).hasSize(1);
758                 },
759                 READ_CONTACTS);
760 
761         // Has READ_EXTERNAL_STORAGE can access the document.
762         SystemUtil.runWithShellPermissionIdentity(
763                 () -> {
764                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
765 
766                     // Can get the document
767                     AppSearchBatchResult<String, GenericDocument> result = mGlobalSearchSession
768                             .getByDocumentIdAsync(PKG_A, "database",
769                                     new GetByDocumentIdRequest.Builder("namespace")
770                                             .addIds("id1")
771                                             .build()).get();
772                     assertThat(result.getSuccesses()).hasSize(1);
773                 },
774                 READ_EXTERNAL_STORAGE);
775     }
776 
777     @Test
testAllowPermissions_sameError()778     public void testAllowPermissions_sameError() throws Exception {
779         // Try to get document before we put them, this is not found error.
780         mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
781         AppSearchBatchResult<String, GenericDocument> nonExistentResult = mGlobalSearchSession
782                 .getByDocumentIdAsync(PKG_A, "database",
783                         new GetByDocumentIdRequest.Builder("namespace")
784                                 .addIds("id1")
785                                 .build()).get();
786         assertThat(nonExistentResult.isSuccess()).isFalse();
787         assertThat(nonExistentResult.getSuccesses()).isEmpty();
788         assertThat(nonExistentResult.getFailures()).containsKey("id1");
789 
790         // Index a global searchable document in pkg_A and set it needs READ_SMS to read it.
791         indexGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1",
792                 ImmutableSet.of(ImmutableSet.of(SetSchemaRequest.READ_SMS)));
793 
794         // Try to get document w/o permission, this is unAuthority error.
795         mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
796         AppSearchBatchResult<String, GenericDocument> unAuthResult = mGlobalSearchSession
797                 .getByDocumentIdAsync(PKG_A, "database",
798                         new GetByDocumentIdRequest.Builder("namespace")
799                                 .addIds("id1")
800                                 .build()).get();
801         assertThat(unAuthResult.isSuccess()).isFalse();
802         assertThat(unAuthResult.getSuccesses()).isEmpty();
803         assertThat(unAuthResult.getFailures()).containsKey("id1");
804 
805         // The error messages must be same.
806         assertThat(unAuthResult.getFailures().get("id1").getErrorMessage())
807                 .isEqualTo(nonExistentResult.getFailures().get("id1").getErrorMessage());
808     }
809 
810     @Test
testGlobalGetById_withAccess()811     public void testGlobalGetById_withAccess() throws Exception {
812         indexGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1");
813 
814         SystemUtil.runWithShellPermissionIdentity(
815                 () -> {
816                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
817 
818                     // Can get the document
819                     AppSearchBatchResult<String, GenericDocument> result = mGlobalSearchSession
820                             .getByDocumentIdAsync(PKG_A, DB_NAME,
821                                     new GetByDocumentIdRequest.Builder(NAMESPACE_NAME)
822                                             .addIds("id1")
823                                             .build()).get();
824 
825                     assertThat(result.getSuccesses()).hasSize(1);
826 
827                     // Can't get non existent document
828                     AppSearchBatchResult<String, GenericDocument> nonExistent = mGlobalSearchSession
829                             .getByDocumentIdAsync(PKG_A, DB_NAME,
830                                     new GetByDocumentIdRequest.Builder(NAMESPACE_NAME)
831                                             .addIds("id2")
832                                             .build()).get();
833 
834                     assertThat(nonExistent.isSuccess()).isFalse();
835                     assertThat(nonExistent.getSuccesses()).isEmpty();
836                 },
837                 READ_GLOBAL_APP_SEARCH_DATA);
838     }
839 
840     @Test
testGlobalGetById_withoutAccess()841     public void testGlobalGetById_withoutAccess() throws Exception {
842         indexNotGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1");
843 
844         SystemUtil.runWithShellPermissionIdentity(
845                 () -> {
846                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
847 
848                     // Can't get the document
849                     AppSearchBatchResult<String, GenericDocument> result = mGlobalSearchSession
850                             .getByDocumentIdAsync(PKG_A, DB_NAME,
851                                     new GetByDocumentIdRequest.Builder(NAMESPACE_NAME)
852                                             .addIds("id1")
853                                             .build()).get();
854                     assertThat(result.isSuccess()).isFalse();
855                     assertThat(result.getSuccesses()).isEmpty();
856                     assertThat(result.getFailures()).containsKey("id1");
857                 },
858                 READ_GLOBAL_APP_SEARCH_DATA);
859     }
860 
861     @Test
testGlobalGetById_sameErrorMessages()862     public void testGlobalGetById_sameErrorMessages() throws Exception {
863         AtomicReference<String> errorMessageNonExistent = new AtomicReference<>();
864         AtomicReference<String> errorMessageUnauth = new AtomicReference<>();
865 
866         // Can't get the document because we haven't added it yet
867         SystemUtil.runWithShellPermissionIdentity(
868                 () -> {
869                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
870 
871                     AppSearchBatchResult<String, GenericDocument> nonExistentResult =
872                             mGlobalSearchSession.getByDocumentIdAsync(PKG_A, DB_NAME,
873                                     new GetByDocumentIdRequest.Builder(NAMESPACE_NAME)
874                                             .addIds("id1")
875                                             .build()).get();
876                     assertThat(nonExistentResult.isSuccess()).isFalse();
877                     assertThat(nonExistentResult.getSuccesses()).isEmpty();
878                     assertThat(nonExistentResult.getFailures()).containsKey("id1");
879                     errorMessageNonExistent.set(
880                             nonExistentResult.getFailures().get("id1").getErrorMessage());
881                 },
882                 READ_GLOBAL_APP_SEARCH_DATA);
883 
884         indexNotGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1");
885 
886         // Can't get the document because the document is not globally searchable
887         SystemUtil.runWithShellPermissionIdentity(
888                 () -> {
889                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
890 
891                     AppSearchBatchResult<String, GenericDocument> unAuthResult =
892                             mGlobalSearchSession.getByDocumentIdAsync(PKG_A, DB_NAME,
893                                     new GetByDocumentIdRequest.Builder(NAMESPACE_NAME)
894                                             .addIds("id1")
895                                             .build()).get();
896                     assertThat(unAuthResult.isSuccess()).isFalse();
897                     assertThat(unAuthResult.getSuccesses()).isEmpty();
898                     assertThat(unAuthResult.getFailures()).containsKey("id1");
899                     errorMessageUnauth.set(
900                             unAuthResult.getFailures().get("id1").getErrorMessage());
901                 },
902                 READ_GLOBAL_APP_SEARCH_DATA);
903 
904         // try adding a global doc here to make sure non-global querier can't get it
905         // and same error message
906         clearData(PKG_A, DB_NAME);
907         indexGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1");
908 
909         // Can't get the document because we don't have global permissions
910         mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
911 
912         AppSearchBatchResult<String, GenericDocument> noGlobalResult = mGlobalSearchSession
913                 .getByDocumentIdAsync(PKG_A, DB_NAME,
914                         new GetByDocumentIdRequest.Builder(NAMESPACE_NAME)
915                                 .addIds("id1")
916                                 .build()).get();
917         assertThat(noGlobalResult.isSuccess()).isFalse();
918         assertThat(noGlobalResult.getSuccesses()).isEmpty();
919         assertThat(noGlobalResult.getFailures()).containsKey("id1");
920 
921         // compare error messages
922         assertThat(errorMessageNonExistent.get()).isEqualTo(errorMessageUnauth.get());
923         assertThat(errorMessageNonExistent.get())
924                 .isEqualTo(noGlobalResult.getFailures().get("id1").getErrorMessage());
925     }
926 
927     @Test
testGlobalGetById_requireAllVisibleToConfig_pkgAndPermission()928     public void testGlobalGetById_requireAllVisibleToConfig_pkgAndPermission() throws Exception {
929         String ctsPackageName = mContext.getPackageName();
930         PackageIdentifier ctsPackage =
931                 new PackageIdentifier(
932                         ctsPackageName,
933                         PackageUtil.getSelfPackageSha256Cert(mContext)
934                 );
935         PackageIdentifier otherPackage =
936                 new PackageIdentifier("com.android.other", new byte[32]);
937         // index global searchable document in pkg_A and set it requires READ_SMS and accessible
938         // to cts package
939         indexGloballySearchableDocumentVisibleToConfig(PKG_A, DB_NAME, NAMESPACE_NAME, "id",
940                 ImmutableSet.of(ctsPackage, otherPackage),
941                 ImmutableSet.of(ImmutableSet.of(SetSchemaRequest.READ_SMS)),
942                 /*publicAclPackage=*/null);
943 
944         // Can get document with READ_SMS permission.
945         SystemUtil.runWithShellPermissionIdentity(
946                 () -> {
947                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
948 
949                     // Can get the document "id1".
950                     AppSearchBatchResult<String, GenericDocument> result = mGlobalSearchSession
951                             .getByDocumentIdAsync(PKG_A, "database",
952                                     new GetByDocumentIdRequest.Builder("namespace")
953                                             .addIds("id")
954                                             .build()).get();
955                     assertThat(result.getSuccesses()).hasSize(1);
956                 },
957                 READ_SMS);
958 
959         // Cann't get document with READ_CALENDAR permission.
960         SystemUtil.runWithShellPermissionIdentity(
961                 () -> {
962                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
963 
964                     // Can't get the document "id2", even if we are while list package, but we don't
965                     // hold required permission.
966                     AppSearchBatchResult<String, GenericDocument> result = mGlobalSearchSession
967                             .getByDocumentIdAsync(PKG_A, "database",
968                                 new GetByDocumentIdRequest.Builder("namespace")
969                                     .addIds("id")
970                                     .build()).get();
971                     assertThat(result.getSuccesses()).isEmpty();
972                 },
973                 READ_CALENDAR);
974 
975         // Update the schema visibility setting and index global searchable document in pkg_A
976         // set it requires READ_SMS and NOT accessible to cts package
977         indexGloballySearchableDocumentVisibleToConfig(PKG_A, DB_NAME, NAMESPACE_NAME, "id",
978                 ImmutableSet.of(otherPackage),
979                 ImmutableSet.of(ImmutableSet.of(SetSchemaRequest.READ_SMS)),
980                 /*publicAclPackage=*/null);
981         // Cann't get document with READ_SMS permission.
982         SystemUtil.runWithShellPermissionIdentity(
983                 () -> {
984                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
985                     // Can't get the document "id2", even if we hold the required permission but
986                     // we are not in the while list packages.
987                     AppSearchBatchResult<String, GenericDocument> result = mGlobalSearchSession
988                             .getByDocumentIdAsync(PKG_A, "database",
989                                 new GetByDocumentIdRequest.Builder("namespace")
990                                     .addIds("id")
991                                     .build()).get();
992                     assertThat(result.getSuccesses()).isEmpty();
993                 },
994                 READ_SMS);
995     }
996 
997     @Test
testGlobalGetById_requireAllVisibleToConfig_publicAclAndPermission()998     public void testGlobalGetById_requireAllVisibleToConfig_publicAclAndPermission()
999             throws Exception {
1000         String ctsPackageName = mContext.getPackageName();
1001         assertThat(mContext.getPackageManager().canPackageQuery(ctsPackageName, PKG_A)).isTrue();
1002         PackageIdentifier packageA = new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256);
1003         // index global searchable document in pkg_A and set it requires READ_SMS and is public to
1004         // packages that can query other package
1005         indexGloballySearchableDocumentVisibleToConfig(PKG_A, DB_NAME, NAMESPACE_NAME, "id",
1006                 /*visibleToPackages=*/ImmutableSet.of(),
1007                 ImmutableSet.of(ImmutableSet.of(SetSchemaRequest.READ_SMS)),
1008                 packageA);
1009 
1010         // Can get document with READ_SMS permission.
1011         SystemUtil.runWithShellPermissionIdentity(
1012                 () -> {
1013                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1014 
1015                     // Can get the document
1016                     AppSearchBatchResult<String, GenericDocument> result = mGlobalSearchSession
1017                             .getByDocumentIdAsync(PKG_A, "database",
1018                                 new GetByDocumentIdRequest.Builder("namespace")
1019                                     .addIds("id")
1020                                     .build()).get();
1021                     assertThat(result.getSuccesses()).hasSize(1);
1022                 },
1023                 READ_SMS);
1024 
1025         // Cann't get document with READ_CALENDAR permission.
1026         SystemUtil.runWithShellPermissionIdentity(
1027                 () -> {
1028                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1029 
1030                     // Can't get the document, even if we are while list package, but we don't
1031                     // hold required permission.
1032                     AppSearchBatchResult<String, GenericDocument> result = mGlobalSearchSession
1033                             .getByDocumentIdAsync(PKG_A, "database",
1034                                 new GetByDocumentIdRequest.Builder("namespace")
1035                                     .addIds("id")
1036                                     .build()).get();
1037                     assertThat(result.getSuccesses()).isEmpty();
1038                 },
1039                 READ_CALENDAR);
1040 
1041         // Update the schema visibility setting and index global searchable document in pkg_A
1042         // set it requires READ_SMS and is public to packages that can query otherPackage
1043         PackageIdentifier otherPackage =
1044                 new PackageIdentifier("com.android.other", new byte[32]);
1045         indexGloballySearchableDocumentVisibleToConfig(PKG_A, DB_NAME, NAMESPACE_NAME, "id",
1046                 /*visibleToPackages=*/ImmutableSet.of(),
1047                 ImmutableSet.of(ImmutableSet.of(SetSchemaRequest.READ_SMS)),
1048                 otherPackage);
1049         // Cann't get document with READ_SMS permission.
1050         SystemUtil.runWithShellPermissionIdentity(
1051                 () -> {
1052                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1053                     // Can't get the document "id2", even if we hold the required permission but
1054                     // we are not in the while list packages.
1055                     AppSearchBatchResult<String, GenericDocument> result = mGlobalSearchSession
1056                             .getByDocumentIdAsync(PKG_A, "database",
1057                                 new GetByDocumentIdRequest.Builder("namespace")
1058                                     .addIds("id")
1059                                     .build()).get();
1060                     assertThat(result.getSuccesses()).isEmpty();
1061                 },
1062                 READ_SMS);
1063     }
1064 
1065     @Test
testGlobalGetById_requireAllVisibleToConfig_all3Settings()1066     public void testGlobalGetById_requireAllVisibleToConfig_all3Settings()
1067             throws Exception {
1068         String ctsPackageName = mContext.getPackageName();
1069         assertThat(mContext.getPackageManager().canPackageQuery(ctsPackageName, PKG_A)).isTrue();
1070         PackageIdentifier ctsPackage =
1071                 new PackageIdentifier(ctsPackageName,
1072                         PackageUtil.getSelfPackageSha256Cert(mContext));
1073         PackageIdentifier packageA = new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256);
1074         PackageIdentifier otherPackage =
1075                 new PackageIdentifier("com.android.other", new byte[32]);
1076         // index global searchable document in pkg_A and set it requires READ_SMS and accessible
1077         // to cts package and is public to packages that can query other package
1078         indexGloballySearchableDocumentVisibleToConfig(PKG_A, DB_NAME, NAMESPACE_NAME, "id",
1079                 ImmutableSet.of(ctsPackage, otherPackage),
1080                 ImmutableSet.of(ImmutableSet.of(SetSchemaRequest.READ_SMS)),
1081                 packageA);
1082 
1083         // Can get document with READ_SMS permission.
1084         SystemUtil.runWithShellPermissionIdentity(
1085                     () -> {
1086                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1087 
1088                     // Can get the document
1089                     AppSearchBatchResult<String, GenericDocument> result = mGlobalSearchSession
1090                             .getByDocumentIdAsync(PKG_A, "database",
1091                                 new GetByDocumentIdRequest.Builder("namespace")
1092                                     .addIds("id")
1093                                     .build()).get();
1094                     assertThat(result.getSuccesses()).hasSize(1);
1095                 },
1096                 READ_SMS);
1097 
1098         // Cann't get document with READ_CALENDAR permission.
1099         SystemUtil.runWithShellPermissionIdentity(
1100                 () -> {
1101                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1102 
1103                     // Can't get the document "id2", even if we are while list package, but we don't
1104                     // hold required permission.
1105                     AppSearchBatchResult<String, GenericDocument> result = mGlobalSearchSession
1106                             .getByDocumentIdAsync(PKG_A, "database",
1107                                 new GetByDocumentIdRequest.Builder("namespace")
1108                                     .addIds("id1")
1109                                     .build()).get();
1110                     assertThat(result.getSuccesses()).isEmpty();
1111                 },
1112                 READ_CALENDAR);
1113 
1114         // Update the schema visibility setting and index global searchable document in pkg_A.
1115         // set it NOT accessible to cts package
1116         indexGloballySearchableDocumentVisibleToConfig(PKG_A, DB_NAME, NAMESPACE_NAME, "id",
1117                 ImmutableSet.of(otherPackage),
1118                 ImmutableSet.of(ImmutableSet.of(SetSchemaRequest.READ_SMS)),
1119                 packageA);
1120 
1121         // Cann't get document with READ_SMS permission.
1122         SystemUtil.runWithShellPermissionIdentity(
1123                     () -> {
1124                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1125                     // Can't get the document "id2", even if we hold the required permission but
1126                     // we are not in the while list packages.
1127                     AppSearchBatchResult<String, GenericDocument> result = mGlobalSearchSession
1128                             .getByDocumentIdAsync(PKG_A, "database",
1129                                 new GetByDocumentIdRequest.Builder("namespace")
1130                                     .addIds("id")
1131                                     .build()).get();
1132                     assertThat(result.getSuccesses()).isEmpty();
1133                 },
1134                 READ_SMS);
1135 
1136         // Update the schema visibility setting and index global searchable document in pkg_A.
1137         // set it requires READ_SMS and accessible to cts package and require can query to target
1138         // the otherPackage.
1139         indexGloballySearchableDocumentVisibleToConfig(PKG_A, DB_NAME, NAMESPACE_NAME, "id",
1140                 ImmutableSet.of(ctsPackage, otherPackage),
1141                 ImmutableSet.of(ImmutableSet.of(SetSchemaRequest.READ_SMS)),
1142                 /*publicAclPackage=*/otherPackage);
1143 
1144         // Cann't get document with READ_SMS permission.
1145         SystemUtil.runWithShellPermissionIdentity(
1146                 () -> {
1147                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1148                     // Can't get the document "id2", even if we hold the required permission but
1149                     // we are not in the while list packages.
1150                     AppSearchBatchResult<String, GenericDocument> result = mGlobalSearchSession
1151                             .getByDocumentIdAsync(PKG_A, "database",
1152                                 new GetByDocumentIdRequest.Builder("namespace")
1153                                     .addIds("id")
1154                                     .build()).get();
1155                     assertThat(result.getSuccesses()).isEmpty();
1156                 },
1157                 READ_SMS);
1158     }
1159 
1160     @Test
testGlobalGetById_requireAllVisibleToConfig_emptyConfig()1161     public void testGlobalGetById_requireAllVisibleToConfig_emptyConfig()
1162             throws Exception {
1163         // index global searchable document in pkg_A and set it requires empty config. This
1164         // shouldn't accessible by anyone.
1165         indexGloballySearchableDocumentVisibleToConfig(PKG_A, DB_NAME, NAMESPACE_NAME, "id",
1166                 /*visibleToPackages=*/ImmutableSet.of(),
1167                 /*visibleToPermissions=*/ImmutableSet.of(),
1168                 /*publicAclPackage=*/null);
1169 
1170         // Cann't get document with READ_SMS permission.
1171         SystemUtil.runWithShellPermissionIdentity(
1172                 () -> {
1173                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1174 
1175                     AppSearchBatchResult<String, GenericDocument> result = mGlobalSearchSession
1176                             .getByDocumentIdAsync(PKG_A, "database",
1177                                     new GetByDocumentIdRequest.Builder("namespace")
1178                                             .addIds("id")
1179                                             .build()).get();
1180                     assertThat(result.getSuccesses()).isEmpty();
1181                 },
1182                 READ_SMS);
1183     }
1184 
1185     @Test
testGlobalSearch_withAccess()1186     public void testGlobalSearch_withAccess() throws Exception {
1187         indexGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1");
1188         indexGloballySearchableDocument(PKG_B, DB_NAME, NAMESPACE_NAME, "id1");
1189 
1190         SystemUtil.runWithShellPermissionIdentity(
1191                 () -> {
1192                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1193 
1194                     SearchResultsShim searchResults =
1195                             mGlobalSearchSession.search(
1196                                     /*queryExpression=*/ "",
1197                                     new SearchSpec.Builder()
1198                                             .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
1199                                             .addFilterPackageNames(PKG_A, PKG_B)
1200                                             .build());
1201                     List<SearchResult> page = searchResults.getNextPageAsync().get();
1202                     assertThat(page).hasSize(2);
1203 
1204                     ImmutableSet<String> actualPackageNames =
1205                             ImmutableSet.of(
1206                                     page.get(0).getPackageName(), page.get(1).getPackageName());
1207                     assertThat(actualPackageNames).containsExactly(PKG_A, PKG_B);
1208                 },
1209                 READ_GLOBAL_APP_SEARCH_DATA);
1210     }
1211 
1212     @Test
testGlobalSearch_withPartialAccess()1213     public void testGlobalSearch_withPartialAccess() throws Exception {
1214         indexGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1");
1215         indexNotGloballySearchableDocument(PKG_B, DB_NAME, NAMESPACE_NAME, "id1");
1216 
1217         SystemUtil.runWithShellPermissionIdentity(
1218                 () -> {
1219                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1220 
1221                     SearchResultsShim searchResults =
1222                             mGlobalSearchSession.search(
1223                                     /*queryExpression=*/ "",
1224                                     new SearchSpec.Builder()
1225                                             .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
1226                                             .addFilterPackageNames(PKG_A, PKG_B)
1227                                             .build());
1228                     List<SearchResult> page = searchResults.getNextPageAsync().get();
1229                     assertThat(page).hasSize(1);
1230 
1231                     assertThat(page.get(0).getPackageName()).isEqualTo(PKG_A);
1232                 },
1233                 READ_GLOBAL_APP_SEARCH_DATA);
1234     }
1235 
1236     @Test
testGlobalSearch_withPackageFilters()1237     public void testGlobalSearch_withPackageFilters() throws Exception {
1238         indexGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1");
1239         indexGloballySearchableDocument(PKG_B, DB_NAME, NAMESPACE_NAME, "id1");
1240 
1241         SystemUtil.runWithShellPermissionIdentity(
1242                 () -> {
1243                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1244 
1245                     SearchResultsShim searchResults =
1246                             mGlobalSearchSession.search(
1247                                     /*queryExpression=*/ "",
1248                                     new SearchSpec.Builder()
1249                                             .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
1250                                             .addFilterPackageNames(PKG_B)
1251                                             .build());
1252                     List<SearchResult> page = searchResults.getNextPageAsync().get();
1253                     assertThat(page).hasSize(1);
1254 
1255                     assertThat(page.get(0).getPackageName()).isEqualTo(PKG_B);
1256                 },
1257                 READ_GLOBAL_APP_SEARCH_DATA);
1258     }
1259 
1260     @Test
testGlobalSearch_withoutAccess()1261     public void testGlobalSearch_withoutAccess() throws Exception {
1262         indexGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1");
1263         indexGloballySearchableDocument(PKG_B, DB_NAME, NAMESPACE_NAME, "id1");
1264 
1265         mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1266 
1267         SearchResultsShim searchResults =
1268                 mGlobalSearchSession.search(
1269                         /*queryExpression=*/ "",
1270                         new SearchSpec.Builder()
1271                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
1272                                 .addFilterPackageNames(PKG_A, PKG_B)
1273                                 .build());
1274         List<SearchResult> page = searchResults.getNextPageAsync().get();
1275         assertThat(page).isEmpty();
1276     }
1277 
1278     @Test
testGlobalSearch_withJoin_packageFiltering()1279     public void testGlobalSearch_withJoin_packageFiltering() throws Exception {
1280         // This test ensures that we can place documents in separate packages, then join them
1281         // together while specifying package filters.
1282         indexGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "ida");
1283 
1284         // In pkg B, we report an action on the document in pkg A. When we retrieve the pkg A
1285         // document using a joinspec, the action in pkg B should be joined.
1286         String qualifiedId =
1287                 DocumentIdUtil.createQualifiedId(
1288                         PKG_A, DB_NAME, NAMESPACE_NAME, "ida");
1289 
1290         indexActionDocument(PKG_B, DB_NAME, NAMESPACE_NAME, "actionId1", qualifiedId,
1291                 /*globallySearchable*/true);
1292 
1293         AppSearchSchema actionSchema =
1294                 new AppSearchSchema.Builder("PlayAction")
1295                         .addProperty(
1296                                 new StringPropertyConfig.Builder("songId")
1297                                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
1298                                         .setJoinableValueType(StringPropertyConfig
1299                                                 .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
1300                                         .build())
1301                         .build();
1302         mDb.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(actionSchema).build()).get();
1303 
1304         GenericDocument join =
1305                 new GenericDocument.Builder<>(NAMESPACE_NAME, "actionId2", "PlayAction")
1306                         .setPropertyString("songId", qualifiedId)
1307                         .build();
1308 
1309         // In the test package, index an action on that document
1310         checkIsBatchResultSuccess(mDb.putAsync(
1311                 new PutDocumentsRequest.Builder().addGenericDocuments(join).build()));
1312 
1313         SystemUtil.runWithShellPermissionIdentity(
1314                 () -> {
1315 
1316                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1317 
1318                     SearchSpec nestedSearchSpec = new SearchSpec.Builder()
1319                             .addFilterPackageNames(PKG_B)
1320                             .build();
1321                     JoinSpec js = new JoinSpec.Builder("songId")
1322                             .setNestedSearch("", nestedSearchSpec)
1323                             .build();
1324 
1325                     SearchResultsShim searchResults =
1326                             mGlobalSearchSession.search(
1327                                     /*queryExpression=*/ "",
1328                                     new SearchSpec.Builder()
1329                                             .addFilterPackageNames(PKG_A)
1330                                             .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
1331                                             .setJoinSpec(js)
1332                                             .build());
1333 
1334                     List<SearchResult> page = searchResults.getNextPageAsync().get();
1335                     assertThat(page).hasSize(1);
1336 
1337                     SearchResult searchResult = page.get(0);
1338 
1339                     assertThat(searchResult.getGenericDocument().getId()).isEqualTo("ida");
1340 
1341                     assertThat(searchResult.getJoinedResults()).hasSize(1);
1342                     SearchResult joined = page.get(0).getJoinedResults().get(0);
1343                     assertThat(joined.getGenericDocument().getId()).isEqualTo("actionId1");
1344                 },
1345                 READ_GLOBAL_APP_SEARCH_DATA);
1346     }
1347 
1348     @Test
testGlobalSearch_withJoin()1349     public void testGlobalSearch_withJoin() throws Exception {
1350         indexGloballySearchableDocument(PKG_B, DB_NAME, NAMESPACE_NAME, "idb");
1351         indexGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "ida");
1352 
1353         // In pkg B, we report an action on the document in pkg A. When we retrieve the pkg A
1354         // document using a joinspec, the action in pkg B should be joined.
1355         String qualifiedId =
1356                 DocumentIdUtil.createQualifiedId(
1357                         PKG_A, DB_NAME, NAMESPACE_NAME, "ida");
1358 
1359         indexActionDocument(PKG_B, DB_NAME, NAMESPACE_NAME, "actionId", qualifiedId,
1360                 /*globallySearchable*/true);
1361 
1362         SystemUtil.runWithShellPermissionIdentity(
1363                 () -> {
1364 
1365                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1366 
1367                     SearchSpec nestedSearchSpec = new SearchSpec.Builder()
1368                             .addFilterPackageNames(PKG_A, PKG_B)
1369                             .build();
1370                     JoinSpec js = new JoinSpec.Builder("songId")
1371                             .setNestedSearch("", nestedSearchSpec)
1372                             .build();
1373 
1374                     // Here, we rank based on document creation timestamp. Since it's ascending,
1375                     // the document in package B should be first.
1376                     SearchResultsShim searchResults =
1377                             mGlobalSearchSession.search(
1378                                     /*queryExpression=*/ "",
1379                                     new SearchSpec.Builder()
1380                                             .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
1381                                             .setRankingStrategy(SearchSpec
1382                                                     .RANKING_STRATEGY_CREATION_TIMESTAMP)
1383                                             .setOrder(SearchSpec.ORDER_ASCENDING)
1384                                             .addFilterPackageNames(PKG_A, PKG_B)
1385                                             .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
1386                                             .setJoinSpec(js)
1387                                             .build());
1388 
1389                     List<SearchResult> page = searchResults.getNextPageAsync().get();
1390                     assertThat(page).hasSize(2);
1391 
1392                     assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("idb");
1393 
1394                     assertThat(page.get(1).getJoinedResults()).hasSize(1);
1395                     assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("ida");
1396 
1397                     SearchResult joined = page.get(1).getJoinedResults().get(0);
1398                     assertThat(joined.getGenericDocument().getId()).isEqualTo("actionId");
1399 
1400                     // Here, we rank based on the number of joined documents a parent document has.
1401                     // The results should be in a different order this time.
1402                     js = new JoinSpec.Builder("songId")
1403                             .setNestedSearch("", nestedSearchSpec)
1404                             .setAggregationScoringStrategy(JoinSpec
1405                                     .AGGREGATION_SCORING_RESULT_COUNT)
1406                             .build();
1407 
1408                     searchResults = mGlobalSearchSession.search(
1409                             /*queryExpression=*/ "",
1410                             new SearchSpec.Builder()
1411                                     .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
1412                                     .setRankingStrategy(SearchSpec
1413                                             .RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
1414                                     .addFilterPackageNames(PKG_A, PKG_B)
1415                                     .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
1416                                     .setJoinSpec(js)
1417                                     .build());
1418 
1419                     page = searchResults.getNextPageAsync().get();
1420                     assertThat(page).hasSize(2);
1421 
1422                     assertThat(page.get(0).getJoinedResults()).hasSize(1);
1423                     assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("ida");
1424 
1425                     joined = page.get(0).getJoinedResults().get(0);
1426                     assertThat(joined.getGenericDocument().getId()).isEqualTo("actionId");
1427 
1428                     assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("idb");
1429                 },
1430                 READ_GLOBAL_APP_SEARCH_DATA);
1431     }
1432 
1433     @Test
testGlobalSearch_withJoin_partialAccess()1434     public void testGlobalSearch_withJoin_partialAccess() throws Exception {
1435 
1436         indexGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "ida");
1437 
1438         String qualifiedId =
1439                 DocumentIdUtil.createQualifiedId(
1440                         PKG_A, DB_NAME, NAMESPACE_NAME, "ida");
1441         // Reporting an action on a globally searchable document, but the action itself isn't
1442         // globally searchable. Searching with joinspec shouldn't return the action document.
1443 
1444         // In pkg B, we report an action on the document in pkg A. When we retrieve the pkg A
1445         // document using a joinspec, the action in pkg B should be joined.
1446 
1447         // here, we index an action document inaccessible from the querier
1448         indexActionDocument(PKG_B, DB_NAME, NAMESPACE_NAME, "actionId", qualifiedId,
1449                 /*globallySearchable*/false);
1450 
1451         SystemUtil.runWithShellPermissionIdentity(
1452                 () -> {
1453 
1454                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1455 
1456                     SearchSpec nestedSearchSpec = new SearchSpec.Builder()
1457                             .addFilterPackageNames(PKG_A, PKG_B)
1458                             .build();
1459                     JoinSpec js =
1460                             new JoinSpec.Builder("songId")
1461                                     .setNestedSearch("", nestedSearchSpec)
1462                                     .setAggregationScoringStrategy(JoinSpec
1463                                             .AGGREGATION_SCORING_RESULT_COUNT)
1464                                     .build();
1465 
1466                     SearchResultsShim searchResults =
1467                             mGlobalSearchSession.search(
1468                                     /*queryExpression=*/ "",
1469                                     new SearchSpec.Builder()
1470                                             .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
1471                                             .setRankingStrategy(SearchSpec
1472                                                     .RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
1473                                             .addFilterPackageNames(PKG_A, PKG_B)
1474                                             .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
1475                                             .setJoinSpec(js)
1476                                             .build());
1477 
1478                     List<SearchResult> page = searchResults.getNextPageAsync().get();
1479                     assertThat(page).hasSize(1);
1480 
1481                     assertThat(page.get(0).getJoinedResults()).hasSize(0);
1482                     assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("ida");
1483                 },
1484                 READ_GLOBAL_APP_SEARCH_DATA);
1485     }
1486 
1487     @Test
testGlobalSearch_withJoin_withoutAccess()1488     public void testGlobalSearch_withJoin_withoutAccess() throws Exception {
1489 
1490         mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1491 
1492         // Insert schema
1493         mDb.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
1494                 .get();
1495 
1496         // Insert two docs
1497         GenericDocument document1 =
1498                 new GenericDocument.Builder<>(NAMESPACE_NAME, "id1", AppSearchEmail.SCHEMA_TYPE)
1499                         .build();
1500         mDb.putAsync(
1501                         new PutDocumentsRequest.Builder().addGenericDocuments(document1).build())
1502                 .get();
1503 
1504         String qualifiedId =
1505                 DocumentIdUtil.createQualifiedId(
1506                         PKG_A, DB_NAME, NAMESPACE_NAME, "id1");
1507         indexActionDocument(PKG_B, DB_NAME, NAMESPACE_NAME, "actionId", qualifiedId,
1508                 /*globallySearchable*/true);
1509 
1510         // Query the data
1511         mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1512 
1513         SearchSpec nestedSearchSpec = new SearchSpec.Builder()
1514                 .addFilterPackageNames(PKG_A, PKG_B)
1515                 .build();
1516         JoinSpec js =
1517                 new JoinSpec.Builder("songId")
1518                         .setNestedSearch("", nestedSearchSpec)
1519                         .setAggregationScoringStrategy(JoinSpec
1520                                 .AGGREGATION_SCORING_RESULT_COUNT)
1521                         .build();
1522 
1523         SearchSpec searchSpec = new SearchSpec.Builder()
1524                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
1525                 .setRankingStrategy(SearchSpec
1526                         .RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
1527                 .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
1528                 .setJoinSpec(js)
1529                 .build();
1530 
1531         SearchResultsShim results = mDb.search("", searchSpec);
1532         List<SearchResult> page = results.getNextPageAsync().get();
1533         assertThat(page).hasSize(1);
1534 
1535         assertThat(page.get(0).getJoinedResults()).hasSize(0);
1536         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
1537     }
1538 
1539     @Test
testGlobalGetSchema_packageAccess_defaultAccess()1540     public void testGlobalGetSchema_packageAccess_defaultAccess() throws Exception {
1541         // 1. Create a schema in the test with default (no) access.
1542         mDb.setSchemaAsync(
1543                         new SetSchemaRequest.Builder()
1544                                 .addSchemas(AppSearchEmail.SCHEMA)
1545                                 .build())
1546                 .get();
1547 
1548         // 2. Neither PKG_A nor PKG_B should be able to retrieve the schema.
1549         List<String> schemaStrings = getSchemaAsPackage(PKG_A);
1550         assertThat(schemaStrings).isNull();
1551 
1552         schemaStrings = getSchemaAsPackage(PKG_B);
1553         assertThat(schemaStrings).isNull();
1554     }
1555 
1556     @Test
testGlobalGetSchema_packageAccess_singleAccess()1557     public void testGlobalGetSchema_packageAccess_singleAccess() throws Exception {
1558         // 1. Create a schema in the test with access granted to PKG_A, but not PKG_B.
1559         mDb.setSchemaAsync(
1560                         new SetSchemaRequest.Builder()
1561                                 .addSchemas(AppSearchEmail.SCHEMA)
1562                                 .setSchemaTypeVisibilityForPackage(
1563                                         AppSearchEmail.SCHEMA_TYPE,
1564                                         /*visible=*/ true,
1565                                         new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
1566                                 .build())
1567                 .get();
1568 
1569         // 2. Only PKG_A should be able to retrieve the schema.
1570         List<String> schemaStrings = getSchemaAsPackage(PKG_A);
1571         assertThat(schemaStrings).containsExactly(AppSearchEmail.SCHEMA.toString());
1572 
1573         schemaStrings = getSchemaAsPackage(PKG_B);
1574         assertThat(schemaStrings).isNull();
1575     }
1576 
1577     @Test
testGlobalGetSchema_packageAccess_multiAccess()1578     public void testGlobalGetSchema_packageAccess_multiAccess() throws Exception {
1579         // 1. Create a schema in the test with access granted to PKG_A and PKG_B.
1580         mDb.setSchemaAsync(
1581                         new SetSchemaRequest.Builder()
1582                                 .addSchemas(AppSearchEmail.SCHEMA)
1583                                 .setSchemaTypeVisibilityForPackage(
1584                                         AppSearchEmail.SCHEMA_TYPE,
1585                                         /*visible=*/ true,
1586                                         new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
1587                                 .setSchemaTypeVisibilityForPackage(
1588                                         AppSearchEmail.SCHEMA_TYPE,
1589                                         /*visible=*/ true,
1590                                         new PackageIdentifier(PKG_B, PKG_B_CERT_SHA256))
1591                                 .build())
1592                 .get();
1593 
1594         // 2. Both packages should be able to retrieve the schema.
1595         List<String> schemaStrings = getSchemaAsPackage(PKG_A);
1596         assertThat(schemaStrings).containsExactly(AppSearchEmail.SCHEMA.toString());
1597 
1598         schemaStrings = getSchemaAsPackage(PKG_B);
1599         assertThat(schemaStrings).containsExactly(AppSearchEmail.SCHEMA.toString());
1600     }
1601 
1602     @Test
testGlobalGetSchema_packageAccess_revokeAccess()1603     public void testGlobalGetSchema_packageAccess_revokeAccess() throws Exception {
1604         // 1. Create a schema in the test with access granted to PKG_A.
1605         mDb.setSchemaAsync(
1606                         new SetSchemaRequest.Builder()
1607                                 .addSchemas(AppSearchEmail.SCHEMA)
1608                                 .setSchemaTypeVisibilityForPackage(
1609                                         AppSearchEmail.SCHEMA_TYPE,
1610                                         /*visible=*/ true,
1611                                         new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
1612                                 .build())
1613                 .get();
1614 
1615         // 2. Now revoke that access.
1616         mDb.setSchemaAsync(
1617                         new SetSchemaRequest.Builder()
1618                                 .addSchemas(AppSearchEmail.SCHEMA)
1619                                 .setSchemaTypeVisibilityForPackage(
1620                                         AppSearchEmail.SCHEMA_TYPE,
1621                                         /*visible=*/ false,
1622                                         new PackageIdentifier(PKG_A, PKG_A_CERT_SHA256))
1623                                 .build())
1624                 .get();
1625 
1626         // 3. PKG_A should NOT be able to retrieve the schema.
1627         List<String> schemaStrings = getSchemaAsPackage(PKG_A);
1628         assertThat(schemaStrings).isNull();
1629     }
1630 
1631     @Test
testGlobalGetSchema_globalAccess_singleAccess()1632     public void testGlobalGetSchema_globalAccess_singleAccess() throws Exception {
1633         // 1. Index documents for PKG_A and PKG_B. This will set the schema for each with the
1634         // corresponding access set.
1635         indexGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1");
1636         indexNotGloballySearchableDocument(PKG_B, DB_NAME, NAMESPACE_NAME, "id1");
1637 
1638         SystemUtil.runWithShellPermissionIdentity(
1639                 () -> {
1640                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1641 
1642                     // 2. The schema for PKG_A should be retrievable, but PKG_B should not be.
1643                     GetSchemaResponse response =
1644                             mGlobalSearchSession.getSchemaAsync(PKG_A, DB_NAME).get();
1645                     assertThat(response.getSchemas()).hasSize(1);
1646 
1647                     response = mGlobalSearchSession.getSchemaAsync(PKG_B, DB_NAME).get();
1648                     assertThat(response.getSchemas()).isEmpty();
1649                 },
1650                 READ_GLOBAL_APP_SEARCH_DATA);
1651     }
1652 
1653     @Test
testGlobalGetSchema_globalAccess_multiAccess()1654     public void testGlobalGetSchema_globalAccess_multiAccess() throws Exception {
1655         // 1. Index documents for PKG_A and PKG_B. This will set the schema for each with the
1656         // corresponding access set.
1657         indexGloballySearchableDocument(PKG_A, DB_NAME, NAMESPACE_NAME, "id1");
1658         indexGloballySearchableDocument(PKG_B, DB_NAME, NAMESPACE_NAME, "id1");
1659 
1660         SystemUtil.runWithShellPermissionIdentity(
1661                 () -> {
1662                     mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1663 
1664                     // 2. The schema for both PKG_A and PKG_B should be retrievable.
1665                     GetSchemaResponse response =
1666                             mGlobalSearchSession.getSchemaAsync(PKG_A, DB_NAME).get();
1667                     assertThat(response.getSchemas()).hasSize(1);
1668 
1669                     response = mGlobalSearchSession.getSchemaAsync(PKG_B, DB_NAME).get();
1670                     assertThat(response.getSchemas()).hasSize(1);
1671                 },
1672                 READ_GLOBAL_APP_SEARCH_DATA);
1673     }
1674 
1675     @Test
1676     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testGlobalOpenBlobRead_visibleToGlobalReader()1677     public void testGlobalOpenBlobRead_visibleToGlobalReader() throws Exception {
1678         byte[] data = generateRandomBytes(10); // 10 Bytes
1679         byte[] digest = calculateDigest(data);
1680         AppSearchBlobHandle handle = AppSearchBlobHandle.createWithSha256(
1681                 digest, PKG_A, DB_NAME, NAMESPACE_NAME);
1682 
1683         try {
1684             // Set blob is visible to nothing but global reader.
1685             writeGloballySearchableBlobVisibleToConfig(PKG_A, DB_NAME, NAMESPACE_NAME, data,
1686                     ImmutableSet.of(),
1687                     ImmutableSet.of(),
1688                     /*publicAclPackage=*/null);
1689 
1690             // READ_GLOBAL_APP_SEARCH_DATA could read
1691             SystemUtil.runWithShellPermissionIdentity(
1692                     () -> {
1693                         mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1694 
1695                         try (OpenBlobForReadResponse readResponse = mGlobalSearchSession
1696                                 .openBlobForReadAsync(ImmutableSet.of(handle)).get()) {
1697                             AppSearchBatchResult<AppSearchBlobHandle, ParcelFileDescriptor>
1698                                     readResult = readResponse.getResult();
1699                             assertTrue(readResult.isSuccess());
1700 
1701                             byte[] readBytes = new byte[10]; // 10 Bytes
1702                             ParcelFileDescriptor readPfd = readResult.getSuccesses().get(handle);
1703                             try (InputStream inputStream =
1704                                          new ParcelFileDescriptor.AutoCloseInputStream(readPfd)) {
1705                                 inputStream.read(readBytes);
1706                             }
1707                             assertThat(readBytes).isEqualTo(data);
1708                         }
1709                     },
1710                     READ_GLOBAL_APP_SEARCH_DATA);
1711 
1712             // without READ_GLOBAL_APP_SEARCH_DATA couldn't read
1713             SystemUtil.runWithShellPermissionIdentity(
1714                     () -> {
1715                         mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1716 
1717                         try (OpenBlobForReadResponse readResponse = mGlobalSearchSession
1718                                 .openBlobForReadAsync(ImmutableSet.of(handle)).get()) {
1719                             AppSearchBatchResult<AppSearchBlobHandle, ParcelFileDescriptor>
1720                                     readResult = readResponse.getResult();
1721                             assertFalse(readResult.isSuccess());
1722 
1723                             assertThat(readResult.getFailures().keySet()).containsExactly(handle);
1724                             assertThat(readResult.getFailures().get(handle).getResultCode())
1725                                     .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
1726                             assertThat(readResult.getFailures().get(handle).getErrorMessage())
1727                                     .contains("Cannot find the blob for handle");
1728                         }
1729                     });
1730         } finally {
1731             removeBlob(PKG_A, DB_NAME, NAMESPACE_NAME, data);
1732         }
1733     }
1734 
1735     @Test
1736     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testGlobalOpenBlobRead_notVisibleToGlobalReader()1737     public void testGlobalOpenBlobRead_notVisibleToGlobalReader() throws Exception {
1738         byte[] data = generateRandomBytes(10); // 10 Bytes
1739         byte[] digest = calculateDigest(data);
1740         AppSearchBlobHandle handle = AppSearchBlobHandle.createWithSha256(
1741                 digest, PKG_A, DB_NAME, NAMESPACE_NAME);
1742         try {
1743             writeGloballyNotSearchableBlob(PKG_A, DB_NAME, NAMESPACE_NAME, data);
1744 
1745             // READ_GLOBAL_APP_SEARCH_DATA couldn't read
1746             SystemUtil.runWithShellPermissionIdentity(
1747                     () -> {
1748                         mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1749 
1750                         try (OpenBlobForReadResponse readResponse = mGlobalSearchSession
1751                                 .openBlobForReadAsync(ImmutableSet.of(handle)).get()) {
1752                             AppSearchBatchResult<AppSearchBlobHandle, ParcelFileDescriptor>
1753                                     readResult = readResponse.getResult();
1754                             assertFalse(readResult.isSuccess());
1755 
1756                             assertThat(readResult.getFailures().keySet()).containsExactly(handle);
1757                             assertThat(readResult.getFailures().get(handle).getResultCode())
1758                                     .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
1759                             assertThat(readResult.getFailures().get(handle).getErrorMessage())
1760                                     .contains("Cannot find the blob for handle");
1761                         }
1762                     });
1763         } finally {
1764             removeBlob(PKG_A, DB_NAME, NAMESPACE_NAME, data);
1765         }
1766     }
1767 
1768     @Test
1769     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_BLOB_STORE)
testGlobalOpenBlobRead_visibleToConfig()1770     public void testGlobalOpenBlobRead_visibleToConfig() throws Exception {
1771         byte[] data = generateRandomBytes(10); // 10 Bytes
1772         byte[] digest = calculateDigest(data);
1773         AppSearchBlobHandle handle = AppSearchBlobHandle.createWithSha256(
1774                 digest, PKG_A, DB_NAME, NAMESPACE_NAME);
1775 
1776         String ctsPackageName = mContext.getPackageName();
1777         PackageIdentifier ctsPackage =
1778                 new PackageIdentifier(
1779                         ctsPackageName,
1780                         PackageUtil.getSelfPackageSha256Cert(mContext)
1781                 );
1782         try {
1783             // set it is visible to the config that the caller must be cts test package AND hold
1784             // READ_SMS.
1785             writeGloballySearchableBlobVisibleToConfig(PKG_A, DB_NAME, NAMESPACE_NAME, data,
1786                     ImmutableSet.of(ctsPackage),
1787                     ImmutableSet.of(ImmutableSet.of(SetSchemaRequest.READ_SMS)),
1788                     /*publicAclPackage=*/null);
1789 
1790             // cts test package AND READ_SMS could read
1791             SystemUtil.runWithShellPermissionIdentity(
1792                     () -> {
1793                         mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1794 
1795                         try (OpenBlobForReadResponse readResponse = mGlobalSearchSession
1796                                 .openBlobForReadAsync(ImmutableSet.of(handle)).get()) {
1797                             AppSearchBatchResult<AppSearchBlobHandle, ParcelFileDescriptor>
1798                                     readResult = readResponse.getResult();
1799                             assertTrue(readResult.isSuccess());
1800 
1801                             byte[] readBytes = new byte[10]; // 10 Bytes
1802                             ParcelFileDescriptor readPfd = readResult.getSuccesses().get(handle);
1803                             try (InputStream inputStream =
1804                                         new ParcelFileDescriptor.AutoCloseInputStream(readPfd)) {
1805                                 inputStream.read(readBytes);
1806                             }
1807                             assertThat(readBytes).isEqualTo(data);
1808                         }
1809                     },
1810                     READ_SMS);
1811 
1812             // cts test package AND READ_CALENDAR couldn't read
1813             SystemUtil.runWithShellPermissionIdentity(
1814                     () -> {
1815                         mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1816 
1817                         try (OpenBlobForReadResponse readResponse = mGlobalSearchSession
1818                                 .openBlobForReadAsync(ImmutableSet.of(handle)).get()) {
1819                             AppSearchBatchResult<AppSearchBlobHandle, ParcelFileDescriptor>
1820                                     readResult = readResponse.getResult();
1821                             assertFalse(readResult.isSuccess());
1822 
1823                             assertThat(readResult.getFailures().keySet()).containsExactly(handle);
1824                             assertThat(readResult.getFailures().get(handle).getResultCode())
1825                                     .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
1826                             assertThat(readResult.getFailures().get(handle).getErrorMessage())
1827                                     .contains("Cannot find the blob for handle");
1828                         }
1829                     },
1830                     READ_CALENDAR);
1831         } finally {
1832             removeBlob(PKG_A, DB_NAME, NAMESPACE_NAME, data);
1833         }
1834     }
1835 
1836     @Test
testReportSystemUsage()1837     public void testReportSystemUsage() throws Exception {
1838         // Insert schema
1839         mDb.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
1840                 .get();
1841 
1842         // Insert two docs
1843         GenericDocument document1 =
1844                 new GenericDocument.Builder<>(NAMESPACE_NAME, "id1", AppSearchEmail.SCHEMA_TYPE)
1845                         .build();
1846         GenericDocument document2 =
1847                 new GenericDocument.Builder<>(NAMESPACE_NAME, "id2", AppSearchEmail.SCHEMA_TYPE)
1848                         .build();
1849         mDb.putAsync(
1850                 new PutDocumentsRequest.Builder().addGenericDocuments(document1, document2).build())
1851                 .get();
1852 
1853         // Report some usages. id1 has 2 app and 1 system usage, id2 has 1 app and 2 system usage.
1854         mDb.reportUsageAsync(
1855                         new ReportUsageRequest.Builder(NAMESPACE_NAME, "id1")
1856                                 .setUsageTimestampMillis(10)
1857                                 .build())
1858                 .get();
1859         mDb.reportUsageAsync(
1860                         new ReportUsageRequest.Builder(NAMESPACE_NAME, "id1")
1861                                 .setUsageTimestampMillis(20)
1862                                 .build())
1863                 .get();
1864         mDb.reportUsageAsync(
1865                         new ReportUsageRequest.Builder(NAMESPACE_NAME, "id2")
1866                                 .setUsageTimestampMillis(100)
1867                                 .build())
1868                 .get();
1869 
1870         SystemUtil.runWithShellPermissionIdentity(() -> {
1871             mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1872             mGlobalSearchSession
1873                     .reportSystemUsageAsync(
1874                             new ReportSystemUsageRequest.Builder(
1875                                     mContext.getPackageName(), DB_NAME, NAMESPACE_NAME, "id1")
1876                                     .setUsageTimestampMillis(1000)
1877                                     .build())
1878                     .get();
1879             mGlobalSearchSession
1880                     .reportSystemUsageAsync(
1881                             new ReportSystemUsageRequest.Builder(
1882                                     mContext.getPackageName(), DB_NAME, NAMESPACE_NAME, "id2")
1883                                     .setUsageTimestampMillis(200)
1884                                     .build())
1885                     .get();
1886             mGlobalSearchSession
1887                     .reportSystemUsageAsync(
1888                             new ReportSystemUsageRequest.Builder(
1889                                     mContext.getPackageName(), DB_NAME, NAMESPACE_NAME, "id2")
1890                                     .setUsageTimestampMillis(150)
1891                                     .build())
1892                     .get();
1893         }, READ_GLOBAL_APP_SEARCH_DATA);
1894 
1895         // Query the data
1896         mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1897 
1898         // Sort by app usage count: id1 should win
1899         try (SearchResultsShim results = mDb.search(
1900                 "",
1901                 new SearchSpec.Builder()
1902                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
1903                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_USAGE_COUNT)
1904                         .build())) {
1905             List<SearchResult> page = results.getNextPageAsync().get();
1906             assertThat(page).hasSize(2);
1907             assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
1908             assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id2");
1909         }
1910 
1911         // Sort by app usage timestamp: id2 should win
1912         try (SearchResultsShim results = mDb.search(
1913                 "",
1914                 new SearchSpec.Builder()
1915                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
1916                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP)
1917                         .build())) {
1918             List<SearchResult> page = results.getNextPageAsync().get();
1919             assertThat(page).hasSize(2);
1920             assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id2");
1921             assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id1");
1922         }
1923 
1924         // Sort by system usage count: id2 should win
1925         try (SearchResultsShim results = mDb.search(
1926                 "",
1927                 new SearchSpec.Builder()
1928                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
1929                         .setRankingStrategy(
1930                                 SearchSpec.RANKING_STRATEGY_SYSTEM_USAGE_COUNT)
1931                         .build())) {
1932             List<SearchResult> page = results.getNextPageAsync().get();
1933             assertThat(page).hasSize(2);
1934             assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id2");
1935             assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id1");
1936         }
1937 
1938         // Sort by system usage timestamp: id1 should win
1939         SearchSpec searchSpec = new SearchSpec.Builder()
1940                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
1941                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP)
1942                 .build();
1943         try (SearchResultsShim results = mDb.search("", searchSpec)) {
1944             List<SearchResult> page = results.getNextPageAsync().get();
1945             assertThat(page).hasSize(2);
1946             assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
1947             assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id2");
1948         }
1949     }
1950 
1951     @Test
testRemoveObserver_otherPackagesNotRemoved()1952     public void testRemoveObserver_otherPackagesNotRemoved() throws Exception {
1953         final String fakePackage = "com.android.appsearch.fake.package";
1954         TestObserverCallback observer = new TestObserverCallback();
1955 
1956         // Set up schema
1957         mGlobalSearchSession = createGlobalSearchSessionAsync(mContext);
1958         mDb.setSchemaAsync(new SetSchemaRequest.Builder()
1959                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
1960 
1961         // Register this observer twice, on different packages.
1962         Executor executor = MoreExecutors.directExecutor();
1963         mGlobalSearchSession.registerObserverCallback(
1964                 mContext.getPackageName(),
1965                 new ObserverSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build(),
1966                 executor,
1967                 observer);
1968         mGlobalSearchSession.registerObserverCallback(
1969                 /*targetPackageName=*/fakePackage,
1970                 new ObserverSpec.Builder().addFilterSchemas("Gift").build(),
1971                 executor,
1972                 observer);
1973 
1974         // Make sure everything is empty
1975         assertThat(observer.getSchemaChanges()).isEmpty();
1976         assertThat(observer.getDocumentChanges()).isEmpty();
1977 
1978         // Index some documents
1979         AppSearchEmail email1 = new AppSearchEmail.Builder(NAMESPACE_NAME, "id1").build();
1980         AppSearchEmail email2 =
1981                 new AppSearchEmail.Builder(NAMESPACE_NAME, "id2").setBody("caterpillar").build();
1982         AppSearchEmail email3 =
1983                 new AppSearchEmail.Builder(NAMESPACE_NAME, "id3").setBody("foo").build();
1984 
1985         checkIsBatchResultSuccess(
1986                 mDb.putAsync(new PutDocumentsRequest.Builder()
1987                         .addGenericDocuments(email1).build()));
1988 
1989         // Make sure the notifications were received.
1990         observer.waitForNotificationCount(1);
1991 
1992         assertThat(observer.getSchemaChanges()).isEmpty();
1993         assertThat(observer.getDocumentChanges()).containsExactly(
1994                 new DocumentChangeInfo(
1995                         mContext.getPackageName(),
1996                         DB_NAME,
1997                         NAMESPACE_NAME,
1998                         AppSearchEmail.SCHEMA_TYPE,
1999                         ImmutableSet.of("id1")));
2000         observer.clear();
2001 
2002         // Unregister observer from com.example.package
2003         mGlobalSearchSession.unregisterObserverCallback("com.example.package", observer);
2004 
2005         // Index some more documents
2006         assertThat(observer.getDocumentChanges()).isEmpty();
2007         checkIsBatchResultSuccess(
2008                 mDb.putAsync(
2009                         new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
2010 
2011         // Make sure data was still received
2012         observer.waitForNotificationCount(1);
2013         assertThat(observer.getDocumentChanges()).containsExactly(
2014                 new DocumentChangeInfo(
2015                         mContext.getPackageName(),
2016                         DB_NAME,
2017                         NAMESPACE_NAME,
2018                         AppSearchEmail.SCHEMA_TYPE,
2019                         ImmutableSet.of("id2")));
2020         observer.clear();
2021 
2022         // Unregister the final observer
2023         mGlobalSearchSession.unregisterObserverCallback(mContext.getPackageName(), observer);
2024 
2025         // Index some more documents
2026         assertThat(observer.getDocumentChanges()).isEmpty();
2027         checkIsBatchResultSuccess(
2028                 mDb.putAsync(
2029                         new PutDocumentsRequest.Builder().addGenericDocuments(email3).build()));
2030 
2031         // Make sure there have been no further notifications
2032         assertThat(observer.getDocumentChanges()).isEmpty();
2033     }
2034 
getSchemaAsPackage(String pkg)2035     private List<String> getSchemaAsPackage(String pkg) throws Exception {
2036         GlobalSearchSessionServiceCtsTestBase.TestServiceConnection serviceConnection =
2037                 bindToHelperService(pkg);
2038         try {
2039             ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
2040             return commandReceiver.globalGetSchema(mContext.getPackageName(), DB_NAME);
2041         } finally {
2042             serviceConnection.unbind();
2043         }
2044     }
2045 
assertPackageCanAccess(List<GenericDocument> expectedDocuments, String pkg)2046     private void assertPackageCanAccess(List<GenericDocument> expectedDocuments, String pkg)
2047             throws Exception {
2048         GlobalSearchSessionServiceCtsTestBase.TestServiceConnection serviceConnection =
2049                 bindToHelperService(pkg);
2050         try {
2051             ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
2052             List<String> results = commandReceiver.globalSearch(TEXT);
2053             assertThat(results).containsExactlyElementsIn(
2054                     expectedDocuments.stream()
2055                             .map(GenericDocument::toString)
2056                             .collect(Collectors.toList()));
2057         } finally {
2058             serviceConnection.unbind();
2059         }
2060     }
2061 
assertPackageCannotAccess(String pkg)2062     private void assertPackageCannotAccess(String pkg) throws Exception {
2063         assertPackageCanAccess(Collections.emptyList(), pkg);
2064     }
2065 
assertPackageCanAccess(GenericDocument expectedDocument, String pkg)2066     private void assertPackageCanAccess(GenericDocument expectedDocument, String pkg)
2067             throws Exception {
2068         assertPackageCanAccess(ImmutableList.of(expectedDocument), pkg);
2069     }
2070 
indexGloballySearchableDocument(String pkg, String databaseName, String namespace, String id)2071     private void indexGloballySearchableDocument(String pkg, String databaseName, String namespace,
2072             String id) throws Exception {
2073         indexGloballySearchableDocument(pkg, databaseName, namespace, id, ImmutableSet.of());
2074     }
2075 
indexGloballySearchableDocument(String pkg, String databaseName, String namespace, String id, Set<Set<Integer>> visibleToPermissions)2076     private void indexGloballySearchableDocument(String pkg, String databaseName, String namespace,
2077             String id, Set<Set<Integer>> visibleToPermissions) throws Exception {
2078         // binder won't accept Set or Integer, we need to convert to List<Bundle>.
2079         List<Bundle> permissionBundles = new ArrayList<>(visibleToPermissions.size());
2080         for (Set<Integer> allRequiredPermissions : visibleToPermissions) {
2081             Bundle permissionBundle = new Bundle();
2082             permissionBundle.putIntegerArrayList("permission",
2083                     new ArrayList<>(allRequiredPermissions));
2084             permissionBundles.add(permissionBundle);
2085         }
2086         GlobalSearchSessionServiceCtsTestBase.TestServiceConnection serviceConnection =
2087                 bindToHelperService(pkg);
2088         try {
2089             ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
2090             assertThat(commandReceiver.indexGloballySearchableDocument(
2091                     databaseName, namespace, id, permissionBundles)).isTrue();
2092         } finally {
2093             serviceConnection.unbind();
2094         }
2095     }
2096 
indexGloballySearchableDocumentVisibleToConfig(String pkg, String databaseName, String namespace, String id, Set<PackageIdentifier> visibleToPackages, Set<Set<Integer>> visibleToPermissions, PackageIdentifier publicAclPackage)2097     private void indexGloballySearchableDocumentVisibleToConfig(String pkg, String databaseName,
2098             String namespace, String id, Set<PackageIdentifier> visibleToPackages,
2099             Set<Set<Integer>> visibleToPermissions, PackageIdentifier publicAclPackage)
2100             throws Exception {
2101         // PackageIdentifierParcel is hidden, we need to use bundle to pass PackageIdentifier.
2102         List<Bundle> packagesBundles = new ArrayList<>(visibleToPackages.size());
2103         for (PackageIdentifier visibleToPackage: visibleToPackages) {
2104             Bundle packageBundle = new Bundle();
2105             packageBundle.putString("packageName", visibleToPackage.getPackageName());
2106             packageBundle.putByteArray("sha256Cert", visibleToPackage.getSha256Certificate());
2107             packagesBundles.add(packageBundle);
2108         }
2109 
2110         // binder won't accept Set or Integer, we need to convert to List<Bundle>.
2111         List<Bundle> permissionBundles = new ArrayList<>(visibleToPermissions.size());
2112         for (Set<Integer> allRequiredPermissions : visibleToPermissions) {
2113             Bundle permissionBundle = new Bundle();
2114             permissionBundle.putIntegerArrayList("permission",
2115                     new ArrayList<>(allRequiredPermissions));
2116             permissionBundles.add(permissionBundle);
2117         }
2118 
2119         Bundle publicAclBundle = null;
2120         if (publicAclPackage != null) {
2121             publicAclBundle = new Bundle();
2122             publicAclBundle.putString("packageName", publicAclPackage.getPackageName());
2123             publicAclBundle.putByteArray("sha256Cert", publicAclPackage.getSha256Certificate());
2124         }
2125 
2126         GlobalSearchSessionServiceCtsTestBase.TestServiceConnection serviceConnection =
2127                 bindToHelperService(pkg);
2128         try {
2129             ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
2130             assertThat(commandReceiver.indexGloballySearchableDocumentVisibleToConfig(databaseName,
2131                     namespace, id, packagesBundles, permissionBundles, publicAclBundle)).isTrue();
2132         } finally {
2133             serviceConnection.unbind();
2134         }
2135     }
2136 
writeGloballySearchableBlobVisibleToConfig(String pkg, String databaseName, String namespace, byte[] data, Set<PackageIdentifier> visibleToPackages, Set<Set<Integer>> visibleToPermissions, PackageIdentifier publicAclPackage)2137     private void writeGloballySearchableBlobVisibleToConfig(String pkg, String databaseName,
2138             String namespace, byte[] data, Set<PackageIdentifier> visibleToPackages,
2139             Set<Set<Integer>> visibleToPermissions, PackageIdentifier publicAclPackage)
2140             throws Exception {
2141         // PackageIdentifierParcel is hidden, we need to use bundle to pass PackageIdentifier.
2142         List<Bundle> packagesBundles = new ArrayList<>(visibleToPackages.size());
2143         for (PackageIdentifier visibleToPackage: visibleToPackages) {
2144             Bundle packageBundle = new Bundle();
2145             packageBundle.putString("packageName", visibleToPackage.getPackageName());
2146             packageBundle.putByteArray("sha256Cert", visibleToPackage.getSha256Certificate());
2147             packagesBundles.add(packageBundle);
2148         }
2149 
2150         // binder won't accept Set or Integer, we need to convert to List<Bundle>.
2151         List<Bundle> permissionBundles = new ArrayList<>(visibleToPermissions.size());
2152         for (Set<Integer> allRequiredPermissions : visibleToPermissions) {
2153             Bundle permissionBundle = new Bundle();
2154             permissionBundle.putIntegerArrayList("permission",
2155                     new ArrayList<>(allRequiredPermissions));
2156             permissionBundles.add(permissionBundle);
2157         }
2158 
2159         Bundle publicAclBundle = null;
2160         if (publicAclPackage != null) {
2161             publicAclBundle = new Bundle();
2162             publicAclBundle.putString("packageName", publicAclPackage.getPackageName());
2163             publicAclBundle.putByteArray("sha256Cert", publicAclPackage.getSha256Certificate());
2164         }
2165 
2166         GlobalSearchSessionServiceCtsTestBase.TestServiceConnection serviceConnection =
2167                 bindToHelperService(pkg);
2168         try {
2169             ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
2170             assertThat(commandReceiver.writeGloballySearchableBlobVisibleToConfig(pkg, databaseName,
2171                     namespace, data, packagesBundles, permissionBundles, publicAclBundle)).isTrue();
2172         } finally {
2173             serviceConnection.unbind();
2174         }
2175     }
2176 
writeGloballyNotSearchableBlob(String pkg, String databaseName, String namespace, byte[] data)2177     private void writeGloballyNotSearchableBlob(String pkg, String databaseName,
2178             String namespace, byte[] data)
2179             throws Exception {
2180 
2181         GlobalSearchSessionServiceCtsTestBase.TestServiceConnection serviceConnection =
2182                 bindToHelperService(pkg);
2183         try {
2184             ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
2185             assertThat(commandReceiver.writeGloballyNotSearchableBlob(pkg, databaseName,
2186                     namespace, data)).isTrue();
2187         } finally {
2188             serviceConnection.unbind();
2189         }
2190     }
2191 
removeBlob(String pkg, String databaseName, String namespace, byte[] data)2192     private void removeBlob(String pkg, String databaseName, String namespace, byte[] data)
2193             throws Exception {
2194         GlobalSearchSessionServiceCtsTestBase.TestServiceConnection serviceConnection =
2195                 bindToHelperService(pkg);
2196         try {
2197             ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
2198             assertThat(commandReceiver.removeBlob(pkg, databaseName, namespace, data)).isTrue();
2199         } finally {
2200             serviceConnection.unbind();
2201         }
2202     }
2203 
indexNotGloballySearchableDocument( String pkg, String databaseName, String namespace, String id)2204     private void indexNotGloballySearchableDocument(
2205             String pkg, String databaseName, String namespace, String id) throws Exception {
2206         GlobalSearchSessionServiceCtsTestBase.TestServiceConnection serviceConnection =
2207                 bindToHelperService(pkg);
2208         try {
2209             ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
2210             assertThat(commandReceiver
2211                     .indexNotGloballySearchableDocument(databaseName, namespace, id)).isTrue();
2212         } finally {
2213             serviceConnection.unbind();
2214         }
2215     }
2216 
indexActionDocument( String pkg, String databaseName, String namespace, String id, String entityId, boolean globallySearchable)2217     private void indexActionDocument(
2218             String pkg, String databaseName, String namespace, String id, String entityId,
2219             boolean globallySearchable)
2220             throws Exception {
2221 
2222         GlobalSearchSessionServiceCtsTestBase.TestServiceConnection serviceConnection =
2223                 bindToHelperService(pkg);
2224         try {
2225             ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
2226             assertThat(commandReceiver
2227                     .indexAction(databaseName, namespace, id, entityId, globallySearchable))
2228                     .isTrue();
2229         } finally {
2230             serviceConnection.unbind();
2231         }
2232     }
2233 
clearData(String pkg, String databaseName)2234     private void clearData(String pkg, String databaseName) throws Exception {
2235         GlobalSearchSessionServiceCtsTestBase.TestServiceConnection serviceConnection =
2236                 bindToHelperService(pkg);
2237         try {
2238             ICommandReceiver commandReceiver = serviceConnection.getCommandReceiver();
2239             assertThat(commandReceiver.clearData(databaseName)).isTrue();
2240         } finally {
2241             serviceConnection.unbind();
2242         }
2243     }
2244 
bindToHelperService( String pkg)2245     private GlobalSearchSessionServiceCtsTestBase.TestServiceConnection bindToHelperService(
2246             String pkg) {
2247         GlobalSearchSessionServiceCtsTestBase.TestServiceConnection serviceConnection =
2248                 new GlobalSearchSessionServiceCtsTestBase.TestServiceConnection(mContext);
2249         Intent intent = new Intent().setComponent(new ComponentName(pkg, HELPER_SERVICE));
2250         mContext.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
2251         return serviceConnection;
2252     }
2253 
2254     private static class TestServiceConnection implements ServiceConnection {
2255         private final Context mContext;
2256         private final BlockingQueue<IBinder> mBlockingQueue = new LinkedBlockingQueue<>();
2257         private ICommandReceiver mCommandReceiver;
2258 
TestServiceConnection(Context context)2259         TestServiceConnection(Context context) {
2260             mContext = context;
2261         }
2262 
2263         @Override
onServiceConnected(ComponentName componentName, IBinder service)2264         public void onServiceConnected(ComponentName componentName, IBinder service) {
2265             Log.i(TAG, "Service got connected: " + componentName);
2266             mBlockingQueue.offer(service);
2267         }
2268 
2269         @Override
onServiceDisconnected(ComponentName componentName)2270         public void onServiceDisconnected(ComponentName componentName) {
2271             Log.e(TAG, "Service got disconnected: " + componentName);
2272         }
2273 
getService()2274         private IBinder getService() throws Exception {
2275             return mBlockingQueue.poll(TIMEOUT_BIND_SERVICE_SEC, TimeUnit.SECONDS);
2276         }
2277 
getCommandReceiver()2278         public ICommandReceiver getCommandReceiver() throws Exception {
2279             if (mCommandReceiver == null) {
2280                 mCommandReceiver = ICommandReceiver.Stub.asInterface(getService());
2281             }
2282             if (mCommandReceiver == null) {
2283                 Log.e(TAG, "Cannot bind to a service in " + TIMEOUT_BIND_SERVICE_SEC + " second.");
2284             }
2285             return mCommandReceiver;
2286         }
2287 
unbind()2288         public void unbind() {
2289             mCommandReceiver = null;
2290             Log.i(TAG, "Service got unbinded.");
2291             mContext.unbindService(this);
2292         }
2293     }
2294 }
2295