1 /* 2 * Copyright (C) 2024 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 17 package com.android.server.appsearch.appsindexer; 18 19 import static org.mockito.ArgumentMatchers.any; 20 import static org.mockito.ArgumentMatchers.anyInt; 21 import static org.mockito.ArgumentMatchers.anyLong; 22 import static org.mockito.ArgumentMatchers.eq; 23 import static org.mockito.Mockito.when; 24 25 import android.annotation.CurrentTimeMillisLong; 26 import android.annotation.NonNull; 27 import android.app.appsearch.AppSearchManager; 28 import android.app.appsearch.AppSearchSchema; 29 import android.app.appsearch.AppSearchSessionShim; 30 import android.app.appsearch.GlobalSearchSessionShim; 31 import android.app.appsearch.PackageIdentifier; 32 import android.app.appsearch.SearchResult; 33 import android.app.appsearch.SearchResultsShim; 34 import android.app.appsearch.SearchSpec; 35 import android.app.appsearch.SetSchemaRequest; 36 import android.app.appsearch.SetSchemaResponse; 37 import android.app.appsearch.testutil.AppSearchSessionShimImpl; 38 import android.app.appsearch.testutil.GlobalSearchSessionShimImpl; 39 import android.app.usage.UsageEvents; 40 import android.app.usage.UsageStatsManager; 41 import android.content.Context; 42 import android.content.pm.ActivityInfo; 43 import android.content.pm.ApplicationInfo; 44 import android.content.pm.PackageInfo; 45 import android.content.pm.PackageManager; 46 import android.content.pm.ResolveInfo; 47 import android.content.pm.ServiceInfo; 48 import android.content.pm.Signature; 49 import android.content.pm.SigningInfo; 50 import android.content.res.Resources; 51 52 import com.android.server.appsearch.appsindexer.appsearchtypes.AppFunctionStaticMetadata; 53 import com.android.server.appsearch.appsindexer.appsearchtypes.AppOpenEvent; 54 import com.android.server.appsearch.appsindexer.appsearchtypes.MobileApplication; 55 56 import org.mockito.Mockito; 57 58 import java.util.ArrayList; 59 import java.util.Arrays; 60 import java.util.Collections; 61 import java.util.List; 62 import java.util.Objects; 63 import java.util.concurrent.ExecutionException; 64 import java.util.concurrent.ExecutorService; 65 66 class TestUtils { 67 // In the mocking tests, integers are appended to this prefix to create unique package names. 68 public static final String FAKE_PACKAGE_PREFIX = "com.fake.package"; 69 public static final Signature FAKE_SIGNATURE = new Signature("deadbeef"); 70 71 // Represents a schema compatible with MobileApplication. This is used to test compatible schema 72 // upgrades. It is compatible as changing to MobileApplication just adds properties. 73 public static final AppSearchSchema COMPATIBLE_APP_SCHEMA = 74 new AppSearchSchema.Builder(MobileApplication.SCHEMA_TYPE) 75 .addProperty( 76 new AppSearchSchema.StringPropertyConfig.Builder( 77 MobileApplication.APP_PROPERTY_PACKAGE_NAME) 78 .setCardinality( 79 AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL) 80 .setIndexingType( 81 AppSearchSchema.StringPropertyConfig 82 .INDEXING_TYPE_PREFIXES) 83 .setTokenizerType( 84 AppSearchSchema.StringPropertyConfig 85 .TOKENIZER_TYPE_VERBATIM) 86 .build()) 87 .build(); 88 89 // Represents a schema compatible with AppOpenEvent. This is used to test compatible schema 90 // upgrades. It is compatible as changing to AppOpenEvent just adds properties. 91 public static final AppSearchSchema COMPATIBLE_APP_OPEN_EVENT_SCHEMA = 92 new AppSearchSchema.Builder(AppOpenEvent.SCHEMA_TYPE) 93 .addProperty( 94 new AppSearchSchema.StringPropertyConfig.Builder( 95 MobileApplication.APP_PROPERTY_PACKAGE_NAME) 96 .setCardinality( 97 AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL) 98 .setIndexingType( 99 AppSearchSchema.StringPropertyConfig 100 .INDEXING_TYPE_PREFIXES) 101 .setTokenizerType( 102 AppSearchSchema.StringPropertyConfig 103 .TOKENIZER_TYPE_VERBATIM) 104 .build()) 105 .build(); 106 107 // Represents a schema incompatible with MobileApplication. This is used to test incompatible 108 // schema upgrades. It is incompatible as changing to MobileApplication removes the 109 // "NotPackageName" field. 110 public static final AppSearchSchema INCOMPATIBLE_APP_SCHEMA = 111 new AppSearchSchema.Builder(MobileApplication.SCHEMA_TYPE) 112 .addProperty( 113 new AppSearchSchema.StringPropertyConfig.Builder("NotPackageName") 114 .setCardinality( 115 AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL) 116 .setIndexingType( 117 AppSearchSchema.StringPropertyConfig 118 .INDEXING_TYPE_PREFIXES) 119 .setTokenizerType( 120 AppSearchSchema.StringPropertyConfig 121 .TOKENIZER_TYPE_PLAIN) 122 .build()) 123 .build(); 124 125 // Represents a schema incompatible with AppOpenEvent. This is used to test incompatible schema 126 // upgrades. It is incompatible as changing to AppOpenEvent will remove the "NotPackageName" 127 // field. 128 public static final AppSearchSchema INCOMPATIBLE_APP_OPEN_EVENT_SCHEMA = 129 new AppSearchSchema.Builder(AppOpenEvent.SCHEMA_TYPE) 130 .addProperty( 131 new AppSearchSchema.StringPropertyConfig.Builder( 132 "NotPackageName") // Different field name 133 .setCardinality( 134 AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL) 135 .setIndexingType( 136 AppSearchSchema.StringPropertyConfig 137 .INDEXING_TYPE_PREFIXES) 138 .setTokenizerType( 139 AppSearchSchema.StringPropertyConfig 140 .TOKENIZER_TYPE_VERBATIM) 141 .build()) 142 .build(); 143 144 /** 145 * Creates a fake {@link PackageInfo} object. 146 * 147 * @param variant provides variation in the mocked PackageInfo so we can index multiple fake 148 * apps. 149 */ 150 @NonNull createFakePackageInfo(int variant)151 public static PackageInfo createFakePackageInfo(int variant) { 152 String pkgName = FAKE_PACKAGE_PREFIX + variant; 153 PackageInfo packageInfo = new PackageInfo(); 154 packageInfo.packageName = pkgName; 155 packageInfo.versionName = "10.0.0"; 156 packageInfo.lastUpdateTime = variant; 157 SigningInfo signingInfo = Mockito.mock(SigningInfo.class); 158 when(signingInfo.getSigningCertificateHistory()) 159 .thenReturn(new Signature[] {FAKE_SIGNATURE}); 160 packageInfo.signingInfo = signingInfo; 161 162 ApplicationInfo appInfo = new ApplicationInfo(); 163 appInfo.packageName = pkgName; 164 appInfo.className = pkgName + ".FakeActivity"; 165 appInfo.name = "package" + variant; 166 appInfo.versionCode = 10; 167 packageInfo.applicationInfo = appInfo; 168 169 return packageInfo; 170 } 171 172 /** 173 * Creates multiple fake {@link PackageInfo} objects 174 * 175 * @param numApps number of PackageInfos to create. 176 * @see #createFakePackageInfo 177 */ 178 @NonNull createFakePackageInfos(int numApps)179 public static List<PackageInfo> createFakePackageInfos(int numApps) { 180 List<PackageInfo> packageInfoList = new ArrayList<>(); 181 for (int i = 0; i < numApps; i++) { 182 packageInfoList.add(createFakePackageInfo(i)); 183 } 184 return packageInfoList; 185 } 186 187 /** 188 * Generates a mock launch activity resolve info corresponding to the same package created by 189 * {@link #createFakePackageInfo} with the same variant. 190 * 191 * @param variant adds variation in the mocked ResolveInfo so we can index multiple fake apps. 192 */ 193 @NonNull createFakeLaunchResolveInfo(int variant)194 public static ResolveInfo createFakeLaunchResolveInfo(int variant) { 195 String pkgName = FAKE_PACKAGE_PREFIX + variant; 196 ResolveInfo mockResolveInfo = new ResolveInfo(); 197 mockResolveInfo.activityInfo = new ActivityInfo(); 198 mockResolveInfo.activityInfo.packageName = pkgName; 199 mockResolveInfo.activityInfo.name = pkgName + ".FakeActivity"; 200 mockResolveInfo.activityInfo.icon = 42; 201 202 mockResolveInfo.activityInfo.applicationInfo = new ApplicationInfo(); 203 mockResolveInfo.activityInfo.applicationInfo.packageName = pkgName; 204 mockResolveInfo.activityInfo.applicationInfo.name = "Fake Application Name"; // Optional 205 return mockResolveInfo; 206 } 207 208 /** 209 * Generates a mock app function activity resolve info corresponding to the same package created 210 * by {@link #createFakePackageInfo} with the same variant. 211 * 212 * @param variant adds variation in the mocked ResolveInfo so we can index multiple fake apps. 213 */ 214 @NonNull createFakeAppFunctionResolveInfo(int variant)215 public static ResolveInfo createFakeAppFunctionResolveInfo(int variant) { 216 String pkgName = FAKE_PACKAGE_PREFIX + variant; 217 ResolveInfo mockResolveInfo = new ResolveInfo(); 218 mockResolveInfo.serviceInfo = new ServiceInfo(); 219 mockResolveInfo.serviceInfo.packageName = pkgName; 220 mockResolveInfo.serviceInfo.name = pkgName + ".FakeActivity"; 221 222 return mockResolveInfo; 223 } 224 225 /** 226 * Generates multiple mock ResolveInfos. 227 * 228 * @see #createFakeLaunchResolveInfo 229 * @param numApps number of mock ResolveInfos to create 230 */ 231 @NonNull createFakeResolveInfos(int numApps)232 public static List<ResolveInfo> createFakeResolveInfos(int numApps) { 233 List<ResolveInfo> resolveInfoList = new ArrayList<>(); 234 for (int i = 0; i < numApps; i++) { 235 resolveInfoList.add(createFakeLaunchResolveInfo(i)); 236 } 237 return resolveInfoList; 238 } 239 240 /** 241 * Configure a mock {@link PackageManager} to return certain {@link PackageInfo}s and {@link 242 * ResolveInfo}s when getInstalledPackages and queryIntentActivities are called, respectively. 243 */ setupMockPackageManager( @onNull PackageManager pm, @NonNull List<PackageInfo> packages, @NonNull List<ResolveInfo> activities, @NonNull List<ResolveInfo> appFunctionServices)244 public static void setupMockPackageManager( 245 @NonNull PackageManager pm, 246 @NonNull List<PackageInfo> packages, 247 @NonNull List<ResolveInfo> activities, 248 @NonNull List<ResolveInfo> appFunctionServices) 249 throws Exception { 250 Objects.requireNonNull(pm); 251 Objects.requireNonNull(packages); 252 Objects.requireNonNull(activities); 253 when(pm.getInstalledPackages(anyInt())).thenReturn(packages); 254 Resources res = Mockito.mock(Resources.class); 255 when(res.getResourcePackageName(anyInt())).thenReturn("idk"); 256 when(res.getResourceTypeName(anyInt())).thenReturn("type"); 257 when(pm.getResourcesForApplication((ApplicationInfo) any())).thenReturn(res); 258 when(pm.getApplicationLabel(any())).thenReturn("label"); 259 when(pm.queryIntentActivities(any(), eq(0))).then(i -> activities); 260 when(pm.queryIntentServices(any(), eq(0))).then(i -> appFunctionServices); 261 } 262 263 /** 264 * Sets up a mock {@link UsageStatsManager} to return the given {@link UsageEvents} when 265 * queryEvents is called. 266 */ setupMockUsageStatsManager( @onNull UsageStatsManager usm, @NonNull UsageEvents usageEvents)267 public static void setupMockUsageStatsManager( 268 @NonNull UsageStatsManager usm, @NonNull UsageEvents usageEvents) throws Exception { 269 Objects.requireNonNull(usm); 270 Objects.requireNonNull(usageEvents); 271 when(usm.queryEvents(anyLong(), anyLong())).thenReturn(usageEvents); 272 } 273 274 /** Wipes out the apps database. */ removeFakePackageDocuments( @onNull Context context, @NonNull ExecutorService executorService)275 public static void removeFakePackageDocuments( 276 @NonNull Context context, @NonNull ExecutorService executorService) 277 throws ExecutionException, InterruptedException { 278 Objects.requireNonNull(context); 279 Objects.requireNonNull(executorService); 280 281 AppSearchSessionShim db = 282 AppSearchSessionShimImpl.createSearchSessionAsync( 283 context, 284 new AppSearchManager.SearchContext.Builder("apps-db").build(), 285 executorService) 286 .get(); 287 288 SetSchemaResponse unused = 289 db.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()) 290 .get(); 291 } 292 293 /** Wipes out the app open events database. */ removeFakeAppOpenEventDocuments( @onNull Context context, @NonNull ExecutorService executorService)294 public static void removeFakeAppOpenEventDocuments( 295 @NonNull Context context, @NonNull ExecutorService executorService) 296 throws ExecutionException, InterruptedException { 297 Objects.requireNonNull(context); 298 Objects.requireNonNull(executorService); 299 300 AppSearchSessionShim db = 301 AppSearchSessionShimImpl.createSearchSessionAsync( 302 context, 303 new AppSearchManager.SearchContext.Builder("app-open-events-db") 304 .build(), 305 executorService) 306 .get(); 307 308 SetSchemaResponse unused = 309 db.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()) 310 .get(); 311 } 312 313 /** 314 * Search for documents indexed by the Apps Indexer. The database, namespace, and schematype are 315 * all configured. 316 * 317 * @param pageSize The page size to use in the {@link SearchSpec}. By setting to a expected 318 * amount + 1, you can verify that the expected quantity of apps docs are present. 319 */ 320 @NonNull searchAppSearchForApps(int pageSize)321 public static List<SearchResult> searchAppSearchForApps(int pageSize) 322 throws ExecutionException, InterruptedException { 323 GlobalSearchSessionShim globalSession = 324 GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync().get(); 325 SearchSpec allDocumentIdsSpec = 326 new SearchSpec.Builder() 327 .addFilterNamespaces(MobileApplication.APPS_NAMESPACE) 328 // We don't want to search over real indexed apps here, just the ones in the 329 // test 330 .addFilterPackageNames("com.android.appsearch.appsindexertests") 331 .addProjection( 332 SearchSpec.SCHEMA_TYPE_WILDCARD, 333 Collections.singletonList( 334 MobileApplication.APP_PROPERTY_UPDATED_TIMESTAMP)) 335 .setResultCountPerPage(pageSize) 336 .build(); 337 // Don't want to get this confused with real indexed apps. 338 SearchResultsShim results = 339 globalSession.search(/* queryExpression= */ "com.fake.package", allDocumentIdsSpec); 340 return results.getNextPageAsync().get(); 341 } 342 343 /** 344 * Creates an {@link AppSearchSessionShim} for the same database the apps indexer interacts with 345 * for mock packages. This is useful for verifying indexed documents and directly adding 346 * documents. 347 */ 348 @NonNull createFakeAppIndexerSession( @onNull Context context, @NonNull ExecutorService executorService)349 public static AppSearchSessionShim createFakeAppIndexerSession( 350 @NonNull Context context, @NonNull ExecutorService executorService) 351 throws ExecutionException, InterruptedException { 352 Objects.requireNonNull(context); 353 Objects.requireNonNull(executorService); 354 return AppSearchSessionShimImpl.createSearchSessionAsync( 355 context, 356 new AppSearchManager.SearchContext.Builder("apps-db").build(), 357 executorService) 358 .get(); 359 } 360 361 /** 362 * Creates an {@link AppSearchSessionShim} for the same database the app open eventss indexer 363 * interacts with for mock events. 364 */ 365 @NonNull createFakeAppOpenEventsIndexerSession( @onNull Context context, @NonNull ExecutorService executorService)366 public static AppSearchSessionShim createFakeAppOpenEventsIndexerSession( 367 @NonNull Context context, @NonNull ExecutorService executorService) 368 throws ExecutionException, InterruptedException { 369 Objects.requireNonNull(context); 370 Objects.requireNonNull(executorService); 371 return AppSearchSessionShimImpl.createSearchSessionAsync( 372 context, 373 new AppSearchManager.SearchContext.Builder("app-open-events-db").build(), 374 executorService) 375 .get(); 376 } 377 378 /** 379 * Generates a mock {@link MobileApplication} corresponding to the same package created by 380 * {@link #createFakePackageInfo} with the same variant. 381 * 382 * @param variant adds variation to the MobileApplication document. 383 */ 384 @NonNull createFakeMobileApplication(int variant)385 public static MobileApplication createFakeMobileApplication(int variant) { 386 return new MobileApplication.Builder( 387 FAKE_PACKAGE_PREFIX + variant, FAKE_SIGNATURE.toByteArray()) 388 .setDisplayName("Fake Application Name") 389 .setIconUri("https://cs.android.com") 390 .setClassName(".class") 391 .setUpdatedTimestampMs(variant) 392 .setAlternateNames("Mock") 393 .build(); 394 } 395 396 /** 397 * Generates a mock {@link AppOpenEvent} document. 398 * 399 * @param timestamp the timestamp of the AppOpenEvent document. 400 * @return a {@link AppOpenEvent} document with the given timestamp. 401 */ 402 @NonNull createFakeAppOpenEvent(@urrentTimeMillisLong long timestamp)403 public static AppOpenEvent createFakeAppOpenEvent(@CurrentTimeMillisLong long timestamp) { 404 return AppOpenEvent.create(FAKE_PACKAGE_PREFIX, timestamp); 405 } 406 407 /** 408 * Generates multiple mock {@link MobileApplication} objects. 409 * 410 * @see #createFakeMobileApplication 411 */ 412 @NonNull createMobileApplications(int numApps)413 public static List<MobileApplication> createMobileApplications(int numApps) { 414 List<MobileApplication> appList = new ArrayList<>(); 415 for (int i = 0; i < numApps; i++) { 416 appList.add(createFakeMobileApplication(i)); 417 } 418 return appList; 419 } 420 421 /** 422 * Generates a mock {@link AppFunctionStaticMetadata} corresponding to the same package created 423 * by {@link #createFakePackageInfo} with the same variant. 424 * 425 * @param packageVariant changes the package of the AppFunctionStaticMetadata document. 426 * @param functionVariant changes the function id of the AppFunctionStaticMetadata document. 427 */ 428 @NonNull createFakeAppFunction( int packageVariant, int functionVariant, Context context)429 public static AppFunctionStaticMetadata createFakeAppFunction( 430 int packageVariant, int functionVariant, Context context) { 431 return new AppFunctionStaticMetadata.Builder( 432 FAKE_PACKAGE_PREFIX + packageVariant, 433 "function_id" + functionVariant, 434 context.getPackageName()) 435 .build(); 436 } 437 438 /** 439 * Returns a package identifier representing some mock package. 440 * 441 * @param variant Provides variety in the package name in the same manner as {@link 442 * #createFakePackageInfo} and {@link #createFakeMobileApplication} 443 */ 444 @NonNull createMockPackageIdentifier(int variant)445 public static PackageIdentifier createMockPackageIdentifier(int variant) { 446 return new PackageIdentifier(FAKE_PACKAGE_PREFIX + variant, FAKE_SIGNATURE.toByteArray()); 447 } 448 449 /** Returns multiple package identifiers for use in testing. */ 450 @NonNull createMockPackageIdentifiers(int numApps)451 public static List<PackageIdentifier> createMockPackageIdentifiers(int numApps) { 452 List<PackageIdentifier> packageIdList = new ArrayList<>(); 453 for (int i = 0; i < numApps; i++) { 454 packageIdList.add(createMockPackageIdentifier(i)); 455 } 456 return packageIdList; 457 } 458 459 /** 460 * Creates a mock {@link UsageEvents} object. 461 * 462 * @param events the events to add to the UsageEvents object. 463 * @return a {@link UsageEvents} object with the given events. 464 */ createUsageEvents(UsageEvents.Event... events)465 public static UsageEvents createUsageEvents(UsageEvents.Event... events) { 466 return new UsageEvents(Arrays.asList(events), new String[] {}); 467 } 468 469 /** 470 * Creates a mock {@link UsageEvents} object. 471 * 472 * @param numEvents the number of events to add to the UsageEvents object. 473 * @return a {@link UsageEvents} object with the given events. 474 */ createManyUsageEvents(int numEvents)475 public static UsageEvents createManyUsageEvents(int numEvents) { 476 List<UsageEvents.Event> events = new ArrayList<>(); 477 for (int i = 0; i < numEvents; i++) { 478 events.add( 479 createIndividualUsageEvent( 480 UsageEvents.Event.ACTIVITY_RESUMED, i, "com.fake.package" + i)); 481 } 482 return new UsageEvents(events, new String[] {}); 483 } 484 485 /** 486 * Creates a mock {@link UsageEvents.Event} object. 487 * 488 * @param eventType the event type of the UsageEvents.Event object. 489 * @param timestamp the timestamp of the UsageEvents.Event object. 490 * @param packageName the package name of the UsageEvents.Event object. 491 * @return a {@link UsageEvents.Event} object with the given event type, timestamp, and package 492 * name. 493 */ createIndividualUsageEvent( int eventType, long timestamp, String packageName)494 public static UsageEvents.Event createIndividualUsageEvent( 495 int eventType, long timestamp, String packageName) { 496 UsageEvents.Event e = new UsageEvents.Event(); 497 e.mEventType = eventType; 498 e.mTimeStamp = timestamp; 499 e.mPackage = packageName; 500 return e; 501 } 502 } 503