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