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