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 com.android.cts.appsearch.helper;
17 
18 import static android.app.appsearch.testutil.AppSearchTestUtils.checkIsBatchResultSuccess;
19 import static android.app.appsearch.testutil.AppSearchTestUtils.convertSearchResultsToDocuments;
20 
21 import android.app.Service;
22 import android.app.appsearch.AppSearchBatchResult;
23 import android.app.appsearch.AppSearchManager;
24 import android.app.appsearch.AppSearchSchema;
25 import android.app.appsearch.AppSearchSchema.PropertyConfig;
26 import android.app.appsearch.AppSearchSchema.StringPropertyConfig;
27 import android.app.appsearch.AppSearchSessionShim;
28 import android.app.appsearch.GenericDocument;
29 import android.app.appsearch.GetByDocumentIdRequest;
30 import android.app.appsearch.GetSchemaResponse;
31 import android.app.appsearch.GlobalSearchSessionShim;
32 import android.app.appsearch.PackageIdentifier;
33 import android.app.appsearch.PutDocumentsRequest;
34 import android.app.appsearch.SchemaVisibilityConfig;
35 import android.app.appsearch.SearchResultsShim;
36 import android.app.appsearch.SearchSpec;
37 import android.app.appsearch.SetSchemaRequest;
38 import android.app.appsearch.testutil.AppSearchEmail;
39 import android.app.appsearch.testutil.AppSearchSessionShimImpl;
40 import android.app.appsearch.testutil.GlobalSearchSessionShimImpl;
41 import android.content.Intent;
42 import android.os.Bundle;
43 import android.os.IBinder;
44 import android.util.ArraySet;
45 import android.util.Log;
46 
47 import com.android.cts.appsearch.ICommandReceiver;
48 
49 import java.util.ArrayList;
50 import java.util.Collections;
51 import java.util.List;
52 import java.util.concurrent.Executors;
53 
54 public class AppSearchTestService extends Service {
55 
56     private static final String TAG = "AppSearchTestService";
57     private GlobalSearchSessionShim mGlobalSearchSessionShim;
58 
59     @Override
onCreate()60     public void onCreate() {
61         try {
62             // We call this here so we can pass in a context. If we try to create the session in the
63             // stub, it'll try to grab the context from ApplicationProvider. But that will fail
64             // since this isn't instrumented.
65             mGlobalSearchSessionShim =
66                     GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(this).get();
67 
68         } catch (Exception e) {
69             Log.e(TAG, "Error starting service.", e);
70         }
71     }
72 
73     @Override
onBind(Intent intent)74     public IBinder onBind(Intent intent) {
75         return new CommandReceiver();
76     }
77 
78     private class CommandReceiver extends ICommandReceiver.Stub {
79 
80         @Override
globalSearch(String queryExpression)81         public List<String> globalSearch(String queryExpression) {
82             try {
83                 final SearchSpec searchSpec =
84                         new SearchSpec.Builder()
85                                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
86                                 .build();
87                 SearchResultsShim searchResults =
88                         mGlobalSearchSessionShim.search(queryExpression, searchSpec);
89                 List<GenericDocument> results = convertSearchResultsToDocuments(searchResults);
90 
91                 List<String> resultStrings = new ArrayList<>();
92                 for (GenericDocument doc : results) {
93                     resultStrings.add(doc.toString());
94                 }
95 
96                 return resultStrings;
97             } catch (Exception e) {
98                 Log.e(TAG, "Error issuing global search.", e);
99                 return Collections.emptyList();
100             }
101         }
102 
103         @Override
globalGet( String packageName, String databaseName, String namespace, String id)104         public List<String> globalGet(
105                 String packageName, String databaseName, String namespace, String id) {
106             try {
107                 AppSearchBatchResult<String, GenericDocument> getResult =
108                         mGlobalSearchSessionShim.getByDocumentIdAsync(
109                                 packageName,
110                                 databaseName,
111                                 new GetByDocumentIdRequest.Builder(namespace)
112                                         .addIds(id)
113                                         .build())
114                                 .get();
115 
116                 List<String> resultStrings = new ArrayList<>();
117                 for (String docKey : getResult.getSuccesses().keySet()) {
118                     resultStrings.add(getResult.getSuccesses().get(docKey).toString());
119                 }
120 
121                 return resultStrings;
122             } catch (Exception e) {
123                 Log.e(TAG, "Error issuing global get.", e);
124                 return Collections.emptyList();
125             }
126         }
127 
globalGetSchema(String packageName, String databaseName)128         public List<String> globalGetSchema(String packageName, String databaseName) {
129             try {
130                 GetSchemaResponse response =
131                         mGlobalSearchSessionShim.getSchemaAsync(packageName, databaseName).get();
132                 if (response == null || response.getSchemas().isEmpty()) {
133                     return null;
134                 }
135                 List<String> schemas = new ArrayList(response.getSchemas().size());
136                 for (AppSearchSchema schema : response.getSchemas()) {
137                     schemas.add(schema.toString());
138                 }
139                 return schemas;
140             } catch (Exception e) {
141                 Log.e(TAG, "Error retrieving global schema.", e);
142                 return null;
143             }
144         }
145 
146         @Override
indexGloballySearchableDocument( String databaseName, String namespace, String id, List<Bundle> permissionBundles)147         public boolean indexGloballySearchableDocument(
148                 String databaseName, String namespace, String id, List<Bundle> permissionBundles) {
149             try {
150                 AppSearchSessionShim db =
151                         AppSearchSessionShimImpl.createSearchSessionAsync(
152                                 AppSearchTestService.this,
153                                 new AppSearchManager.SearchContext.Builder(databaseName).build(),
154                                 Executors.newCachedThreadPool())
155                                 .get();
156 
157                 // By default, schemas/documents are globally searchable. We don't purposely set
158                 // setSchemaTypeDisplayedBySystem(false) for this schema
159                 SetSchemaRequest.Builder setSchemaRequestBuilder =
160                         new SetSchemaRequest.Builder()
161                                 .setForceOverride(true)
162                                 .addSchemas(AppSearchEmail.SCHEMA);
163                 for (int i = 0; i < permissionBundles.size(); i++) {
164                     setSchemaRequestBuilder.addRequiredPermissionsForSchemaTypeVisibility(
165                             AppSearchEmail.SCHEMA_TYPE,
166                             new ArraySet<>(permissionBundles.get(i)
167                                     .getIntegerArrayList("permission")));
168                 }
169                 db.setSchemaAsync(setSchemaRequestBuilder.build()).get();
170 
171                 AppSearchEmail emailDocument =
172                         new AppSearchEmail.Builder(namespace, id)
173                                 .setFrom("from@example.com")
174                                 .setTo("to1@example.com", "to2@example.com")
175                                 .setSubject("subject")
176                                 .setBody("this is the body of the email")
177                                 .build();
178                 checkIsBatchResultSuccess(
179                         db.putAsync(
180                                 new PutDocumentsRequest.Builder()
181                                         .addGenericDocuments(emailDocument)
182                                         .build()));
183                 return true;
184             } catch (Exception e) {
185                 Log.e(TAG, "Failed to index globally searchable document.", e);
186             }
187             return false;
188         }
189 
190         /**
191          * Set A schema and index a document with specific visible to config setting to the
192          * given database.
193          *
194          * @param databaseName       The name of database to set the schema.
195          * @param namespace          The namespace of the indexed document
196          * @param id                 The id of the indexed document
197          * @param packageBundles     The VisibleToPackage settings in VisibleToConfig
198          * @param permissionBundles  The VisibleToPermission settings in VisibleToConfig
199          * @param publicAclPackage   The target public acl settings in VisibleToConfig
200          * @return whether this operation is successful.
201          */
202         @Override
indexGloballySearchableDocumentVisibleToConfig( String databaseName, String namespace, String id, List<Bundle> packageBundles, List<Bundle> permissionBundles, Bundle publicAclPackage)203         public boolean indexGloballySearchableDocumentVisibleToConfig(
204                 String databaseName, String namespace, String id, List<Bundle> packageBundles,
205                 List<Bundle> permissionBundles, Bundle publicAclPackage) {
206             try {
207                 AppSearchSessionShim db = AppSearchSessionShimImpl.createSearchSessionAsync(
208                         AppSearchTestService.this,
209                                 new AppSearchManager.SearchContext.Builder(databaseName).build(),
210                                 Executors.newCachedThreadPool())
211                         .get();
212 
213                 // By default, schemas/documents are globally searchable. We don't purposely set
214                 // setSchemaTypeDisplayedBySystem(false) for this schema
215                 SchemaVisibilityConfig.Builder configBuilder = new SchemaVisibilityConfig.Builder();
216                 for (int i = 0; i < packageBundles.size(); i++) {
217                     configBuilder.addAllowedPackage(
218                             new PackageIdentifier(
219                                     packageBundles.get(i).getString("packageName"),
220                                     packageBundles.get(i).getByteArray("sha256Cert")));
221                 }
222                 for (int i = 0; i < permissionBundles.size(); i++) {
223                     configBuilder.addRequiredPermissions(
224                             new ArraySet<>(permissionBundles.get(i)
225                                     .getIntegerArrayList("permission")));
226                 }
227                 if (publicAclPackage != null) {
228                     configBuilder.setPubliclyVisibleTargetPackage(
229                             new PackageIdentifier(
230                                     publicAclPackage.getString("packageName"),
231                                     publicAclPackage.getByteArray("sha256Cert")));
232                 }
233                 db.setSchemaAsync(new SetSchemaRequest.Builder()
234                         .setForceOverride(true)
235                         .addSchemas(AppSearchEmail.SCHEMA)
236                         .addSchemaTypeVisibleToConfig(AppSearchEmail.SCHEMA_TYPE,
237                                 configBuilder.build())
238                         .build()).get();
239 
240                 AppSearchEmail emailDocument =
241                         new AppSearchEmail.Builder(namespace, id)
242                                 .setFrom("from@example.com")
243                                 .setTo("to1@example.com", "to2@example.com")
244                                 .setSubject("subject")
245                                 .setBody("this is the body of the email")
246                                 .build();
247                 checkIsBatchResultSuccess(
248                         db.putAsync(
249                                 new PutDocumentsRequest.Builder()
250                                         .addGenericDocuments(emailDocument)
251                                         .build()));
252                 return true;
253             } catch (Exception e) {
254                 Log.e(TAG, "Failed to index globally searchable document.", e);
255             }
256             return false;
257         }
258 
259         @Override
indexNotGloballySearchableDocument( String databaseName, String namespace, String id)260         public boolean indexNotGloballySearchableDocument(
261                 String databaseName, String namespace, String id) {
262             try {
263                 AppSearchSessionShim db =
264                         AppSearchSessionShimImpl.createSearchSessionAsync(
265                                 AppSearchTestService.this,
266                                 new AppSearchManager.SearchContext.Builder(databaseName).build(),
267                                 Executors.newCachedThreadPool())
268                                 .get();
269 
270                 db.setSchemaAsync(
271                         new SetSchemaRequest.Builder()
272                                 .addSchemas(AppSearchEmail.SCHEMA)
273                                 .setForceOverride(true)
274                                 .setSchemaTypeDisplayedBySystem(
275                                         AppSearchEmail.SCHEMA_TYPE, /*displayed=*/ false)
276                                 .build())
277                         .get();
278 
279                 AppSearchEmail emailDocument =
280                         new AppSearchEmail.Builder(namespace, id)
281                                 .setFrom("from@example.com")
282                                 .setTo("to1@example.com", "to2@example.com")
283                                 .setSubject("subject")
284                                 .setBody("this is the body of the email")
285                                 .build();
286                 checkIsBatchResultSuccess(
287                         db.putAsync(
288                                 new PutDocumentsRequest.Builder()
289                                         .addGenericDocuments(emailDocument)
290                                         .build()));
291                 return true;
292             } catch (Exception e) {
293                 Log.e(TAG, "Failed to index not-globally searchable document.", e);
294             }
295             return false;
296         }
297 
298         @Override
indexAction( String databaseName, String namespace, String id, String entityId, boolean globallySearchable)299         public boolean indexAction(
300                 String databaseName, String namespace, String id, String entityId,
301                 boolean globallySearchable) {
302             try {
303                 AppSearchSessionShim db =
304                         AppSearchSessionShimImpl.createSearchSessionAsync(
305                                         AppSearchTestService.this,
306                                         new AppSearchManager.SearchContext.Builder(databaseName)
307                                                 .build(),
308                                         Executors.newCachedThreadPool())
309                                 .get();
310 
311                 AppSearchSchema actionSchema =
312                         new AppSearchSchema.Builder("PlayAction")
313                                 .addProperty(
314                                         new StringPropertyConfig.Builder("songId")
315                                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
316                                                 .setJoinableValueType(StringPropertyConfig
317                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
318                                                 .build())
319                                 .build();
320 
321                 // By default, schemas/documents are globally searchable. We purposely don't set
322                 // setSchemaTypeDisplayedBySystem(false) for this schema
323                 SetSchemaRequest.Builder setSchemaRequest =
324                         new SetSchemaRequest.Builder()
325                                 .setForceOverride(true)
326                                 .addSchemas(AppSearchEmail.SCHEMA, actionSchema);
327 
328                 if (!globallySearchable) {
329                     setSchemaRequest.setSchemaTypeDisplayedBySystem(
330                                     AppSearchEmail.SCHEMA_TYPE, false)
331                             .setSchemaTypeDisplayedBySystem(
332                                     actionSchema.getSchemaType(), false);
333                 }
334 
335                 db.setSchemaAsync(setSchemaRequest.build()).get();
336 
337                 GenericDocument join =
338                         new GenericDocument.Builder<>(namespace, id, "PlayAction")
339                                 .setPropertyString("songId", entityId)
340                                 .build();
341 
342                 checkIsBatchResultSuccess(
343                         db.putAsync(
344                                 new PutDocumentsRequest.Builder()
345                                         .addGenericDocuments(join)
346                                         .build()));
347 
348                 return true;
349             } catch (Exception e) {
350                 Log.e(TAG, "Failed to index " + (globallySearchable ? "" : "non-")
351                         + "globally searchable action document.", e);
352             }
353             return false;
354         }
355 
356         @Override
setUpPubliclyVisibleDocuments(String targetPackageNameA, byte[] targetPackageCertA, String targetPackageNameB, byte[] targetPackageCertB)357         public boolean setUpPubliclyVisibleDocuments(String targetPackageNameA,
358                 byte[] targetPackageCertA, String targetPackageNameB, byte[] targetPackageCertB) {
359             // We need two schemas, with two different target packages. This way we can test public
360             // visibility.
361 
362             try {
363                 AppSearchSessionShim db = AppSearchSessionShimImpl.createSearchSessionAsync(
364                                 AppSearchTestService.this,
365                                 new AppSearchManager.SearchContext.Builder("database").build(),
366                                 Executors.newCachedThreadPool())
367                         .get();
368 
369                 String schemaNameA = targetPackageNameA + "Schema";
370                 String schemaNameB = targetPackageNameB + "Schema";
371 
372                 AppSearchSchema schemaA = new AppSearchSchema.Builder(schemaNameA)
373                         .addProperty(new StringPropertyConfig.Builder("searchable")
374                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
375                                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
376                                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
377                                 .build()).build();
378 
379                 AppSearchSchema schemaB = new AppSearchSchema.Builder(schemaNameB)
380                         .addProperty(new StringPropertyConfig.Builder("searchable")
381                                 .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
382                                 .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
383                                 .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
384                                 .build()).build();
385 
386                 // Index schemas in the cts package
387                 db.setSchemaAsync(new SetSchemaRequest.Builder()
388                         .addSchemas(schemaA, schemaB)
389                         .setForceOverride(true)
390                         .setPubliclyVisibleSchema(schemaNameA,
391                                 new PackageIdentifier(targetPackageNameA, targetPackageCertA))
392                         .setPubliclyVisibleSchema(schemaNameB,
393                                 new PackageIdentifier(targetPackageNameB, targetPackageCertB))
394                         .build()).get();
395 
396                 GenericDocument docA =
397                         new GenericDocument.Builder<>("namespace", "id1", schemaNameA)
398                                 .setCreationTimestampMillis(0L)
399                                 .setPropertyString("searchable",
400                                         "pineapple from " + targetPackageNameA).build();
401                 GenericDocument docB =
402                         new GenericDocument.Builder<>("namespace", "id2", schemaNameB)
403                                 .setCreationTimestampMillis(0L)
404                                 .setPropertyString("searchable",
405                                         "pineapple from " + targetPackageNameB).build();
406                 checkIsBatchResultSuccess(db.putAsync(new PutDocumentsRequest.Builder()
407                         .addGenericDocuments(docA, docB).build()));
408                 return true;
409             } catch (Exception e) {
410                 Log.e(TAG, "Failed to index publicly searchable document.", e);
411             }
412             return false;
413         }
414 
clearData(String databaseName)415         public boolean clearData(String databaseName) {
416             try {
417                 // Force override with empty schema will clear all previous schemas and their
418                 // documents.
419                 AppSearchSessionShim db =
420                         AppSearchSessionShimImpl.createSearchSessionAsync(
421                                 AppSearchTestService.this,
422                                 new AppSearchManager.SearchContext.Builder(databaseName).build(),
423                                 Executors.newCachedThreadPool())
424                                 .get();
425 
426                 db.setSchemaAsync(
427                         new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
428 
429                 return true;
430             } catch (Exception e) {
431                 Log.e(TAG, "Failed to clear data.", e);
432             }
433             return false;
434         }
435     }
436 }
437