• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 package com.android.adservices.service.common;
17 
18 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PACKAGE_DENY_PROCESS_ERROR_DISABLED;
19 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PACKAGE_DENY_PROCESS_ERROR_FAILED_FILTERING_INSTALLED_PACKAGES;
20 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PACKAGE_DENY_PROCESS_ERROR_FAILED_READING_CACHE;
21 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PACKAGE_DENY_PROCESS_ERROR_FAILED_READING_FILE;
22 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PACKAGE_DENY_PROCESS_ERROR_FAILED_UPDATING_CACHE;
23 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PACKAGE_DENY_PROCESS_ERROR_FAILURE_UNKNOWN;
24 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PACKAGE_DENY_PROCESS_ERROR_NO_FILE_FOUND;
25 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON;
26 
27 import android.annotation.Nullable;
28 import android.content.pm.ApplicationInfo;
29 import android.content.pm.PackageManager;
30 import android.net.Uri;
31 import android.os.Build;
32 
33 import androidx.annotation.NonNull;
34 import androidx.annotation.RequiresApi;
35 import androidx.datastore.guava.GuavaDataStore;
36 
37 import com.android.adservices.LogUtil;
38 import com.android.adservices.concurrency.AdServicesExecutors;
39 import com.android.adservices.download.MobileDataDownloadFactory;
40 import com.android.adservices.errorlogging.ErrorLogUtil;
41 import com.android.adservices.service.Flags;
42 import com.android.adservices.service.FlagsFactory;
43 import com.android.adservices.service.common.compat.FileCompatUtils;
44 import com.android.adservices.service.proto.ApiDenyGroupsForPackage;
45 import com.android.adservices.service.proto.ApiGroupsCache;
46 import com.android.adservices.service.proto.PackageToApiDenyGroupsCacheMap;
47 import com.android.adservices.service.proto.PackageToApiDenyGroupsMap;
48 import com.android.adservices.service.proto.PackageType;
49 import com.android.adservices.shared.common.ApplicationContextSingleton;
50 import com.android.adservices.shared.datastore.ProtoSerializer;
51 import com.android.internal.annotations.VisibleForTesting;
52 
53 import com.google.android.libraries.mobiledatadownload.GetFileGroupRequest;
54 import com.google.android.libraries.mobiledatadownload.MobileDataDownload;
55 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage;
56 import com.google.android.libraries.mobiledatadownload.file.openers.ReadStreamOpener;
57 import com.google.common.util.concurrent.FluentFuture;
58 import com.google.common.util.concurrent.Futures;
59 import com.google.common.util.concurrent.ListenableFuture;
60 import com.google.mobiledatadownload.ClientConfigProto;
61 import com.google.protobuf.ExtensionRegistryLite;
62 
63 import java.io.IOException;
64 import java.util.Collections;
65 import java.util.List;
66 import java.util.Map;
67 import java.util.Set;
68 import java.util.stream.Collectors;
69 
70 @RequiresApi(Build.VERSION_CODES.S)
71 public final class AdPackageDenyResolver {
72     private static final String GROUP_NAME = "package-deny";
73     private static final String PACKAGE_DENY_DATA_STORE =
74             FileCompatUtils.getAdservicesFilename("package_deny_data_store");
75     private final boolean mEnablePackageDeny;
76     @Nullable private final MobileDataDownload mMobileDataDownload;
77     @Nullable private final SynchronousFileStorage mFileStorage;
78 
79     @Nullable
80     private final GuavaDataStore<PackageToApiDenyGroupsCacheMap> mPackageDenyCacheDataStore;
81 
82     // Lazy initialization holder class idiom for static fields as described in Effective Java Item
83     // 83 - this is needed because otherwise the singleton would be initialized in unit tests, even
84     // when they (correctly) call newInstance() instead of getInstance().
85     private static final class FieldHolder {
86         private static final AdPackageDenyResolver sSingleton;
87 
88         static { // static initialization
89             sSingleton = newInstance();
90         }
91     }
92 
AdPackageDenyResolver( @ullable MobileDataDownload mobileDataDownload, @Nullable SynchronousFileStorage synchronousFileStorage, @Nullable GuavaDataStore<PackageToApiDenyGroupsCacheMap> packageDenyCacheDataStore, boolean enablePackageDeny)93     private AdPackageDenyResolver(
94             @Nullable MobileDataDownload mobileDataDownload,
95             @Nullable SynchronousFileStorage synchronousFileStorage,
96             @Nullable GuavaDataStore<PackageToApiDenyGroupsCacheMap> packageDenyCacheDataStore,
97             boolean enablePackageDeny) {
98         LogUtil.d("initializing AdPackageDenyResolver instance");
99         this.mMobileDataDownload = mobileDataDownload;
100         this.mFileStorage = synchronousFileStorage;
101         this.mPackageDenyCacheDataStore = packageDenyCacheDataStore;
102         mEnablePackageDeny = enablePackageDeny;
103     }
104 
105     /**
106      * Creates a new instance of {@link AdPackageDenyResolver}.
107      *
108      * <p>This method uses the `enablePackageDenyMdd` flag from {@link FlagsFactory} to determine
109      * how the `AdPackageDenyResolver` should be initialized.
110      *
111      * <p>If the flag is enabled: - Obtains an MDD instance from {@link MobileDataDownloadFactory}.
112      * - Retrieves a file storage instance from {@link MobileDataDownloadFactory}. - Uses a
113      * `PackageToApiDenyGroupsCacheMapDataStore` (obtained via
114      * `getPackageToApiDenyGroupsCacheMapDataStore()`). - Sets enablement flag to `true`.
115      *
116      * <p>If the flag is disabled: - It creates an instance with all dependencies set to `null` and
117      * the enablement flag set to `false`.
118      *
119      * @return A new instance of `AdPackageDenyResolver` configured based on the
120      *     `enablePackageDenyMdd` flag.
121      */
newInstance()122     private static AdPackageDenyResolver newInstance() {
123         try {
124             Flags flags = FlagsFactory.getFlags();
125             if (flags.getEnablePackageDenyService()) {
126                 return new AdPackageDenyResolver(
127                         MobileDataDownloadFactory.getMdd(flags),
128                         MobileDataDownloadFactory.getFileStorage(),
129                         getPackageToApiDenyGroupsCacheMapDataStore(),
130                         true);
131             }
132         } catch (Exception e) {
133             LogUtil.e("Error initializing AdPackageDenyResolver %s", e.getMessage());
134         }
135         return new AdPackageDenyResolver(null, null, null, false);
136     }
137 
138     @VisibleForTesting
newInstanceForTests( @onNull MobileDataDownload mobileDataDownload, @NonNull SynchronousFileStorage synchronousFileStorage, @NonNull GuavaDataStore<PackageToApiDenyGroupsCacheMap> packageDataStore, boolean enablePackageDeny)139     static AdPackageDenyResolver newInstanceForTests(
140             @NonNull MobileDataDownload mobileDataDownload,
141             @NonNull SynchronousFileStorage synchronousFileStorage,
142             @NonNull GuavaDataStore<PackageToApiDenyGroupsCacheMap> packageDataStore,
143             boolean enablePackageDeny) {
144         return new AdPackageDenyResolver(
145                 mobileDataDownload, synchronousFileStorage, packageDataStore, enablePackageDeny);
146     }
147 
148     private static GuavaDataStore<PackageToApiDenyGroupsCacheMap>
getPackageToApiDenyGroupsCacheMapDataStore()149             getPackageToApiDenyGroupsCacheMapDataStore() {
150         return new GuavaDataStore.Builder(
151                         ApplicationContextSingleton.get(),
152                         PACKAGE_DENY_DATA_STORE,
153                         new ProtoSerializer<PackageToApiDenyGroupsCacheMap>(
154                                 PackageToApiDenyGroupsCacheMap.getDefaultInstance(),
155                                 ExtensionRegistryLite.getEmptyRegistry()))
156                 .setExecutor(AdServicesExecutors.getBackgroundExecutor())
157                 .build();
158     }
159 
160     /**
161      * Returns the singleton instance of {@link AdPackageDenyResolver}.
162      *
163      * <p>This method provides access to the single instance of the `AdPackageDenyResolver` class,
164      * ensuring that all parts of the application use the same resolver object.
165      *
166      * @return The singleton instance of `AdPackageDenyResolver`.
167      */
getInstance()168     public static AdPackageDenyResolver getInstance() {
169         return FieldHolder.sSingleton;
170     }
171 
172     /**
173      * Determines whether an app or SDK should be denied access based on the provided API groups.
174      *
175      * <p>This method checks if access should be denied for a given caller app or SDK based on their
176      * usage of specific API groups. It uses a package deny list loaded from MDD and cached in
177      * `mPackageDenyCacheDataStore`.
178      *
179      * <p>The logic works as follows: 1. **Feature Check:** If the package deny feature
180      * (`mEnablePackageDeny`) is disabled, it logs a message and immediately returns `false`. 2.
181      * **Input Validation:** If `apiGroups` is null or empty, or both `callerAppName` and
182      * `callerSdkName` are null, it returns `false`. 3. **Deny List Check:** It retrieves the
183      * package deny list data from the cache (`mPackageDenyCacheDataStore`). 4. **App and SDK
184      * Check:** It checks if the `callerAppName` or `callerSdkName` exists in the deny list and if
185      * there's any overlap between the provided `apiGroups` and the API groups associated with the
186      * caller app or SDK in the deny list. If an overlap is found, it means the package should be
187      * denied access.
188      *
189      * @param callerAppName The name of the calling app. Can be null or empty.
190      * @param callerSdkName The name of the calling SDK. Can be null or empty.
191      * @param apiGroups The set of API groups being accessed.
192      * @return a {@link ListenableFuture} representing the result of the deny list check. The future
193      *     yields `true` if the package should be denied access, `false` otherwise.
194      */
shouldDenyPackage( String callerAppName, String callerSdkName, Set<String> apiGroups)195     public ListenableFuture<Boolean> shouldDenyPackage(
196             String callerAppName, String callerSdkName, Set<String> apiGroups) {
197         if (!mEnablePackageDeny) {
198             LogUtil.e(
199                     "shouldDenyPackage() is called on package-based deny-list that is disabled"
200                             + " with calling app %s, calling sdk %s and api groups %s",
201                     callerAppName, callerSdkName, apiGroups);
202             PackageDenyMddProcessStatus.logError(PackageDenyMddProcessStatus.DISABLED);
203             return Futures.immediateFuture(false);
204         }
205         if (apiGroups == null
206                 || apiGroups.isEmpty()
207                 || (callerAppName == null && callerSdkName == null)) {
208             return Futures.immediateFuture(false);
209         }
210         return Futures.transform(
211                 mPackageDenyCacheDataStore.getDataAsync(),
212                 packageToApiDenyGroupsCacheMap ->
213                         (callerAppName != null
214                                         && !callerAppName.isBlank()
215                                         && !Collections.disjoint(
216                                                 apiGroups,
217                                                 packageToApiDenyGroupsCacheMap
218                                                         .getAppToApiDenyGroupsCacheMap()
219                                                         .getOrDefault(
220                                                                 callerAppName,
221                                                                 ApiGroupsCache.getDefaultInstance())
222                                                         .getApiGroupList()))
223                                 || (callerSdkName != null
224                                         && !callerSdkName.isBlank()
225                                         && !Collections.disjoint(
226                                                 apiGroups,
227                                                 packageToApiDenyGroupsCacheMap
228                                                         .getSdkToApiDenyGroupsCacheMap()
229                                                         .getOrDefault(
230                                                                 callerSdkName,
231                                                                 ApiGroupsCache.getDefaultInstance())
232                                                         .getApiGroupList())),
233                 AdServicesExecutors.getBackgroundExecutor());
234     }
235 
236     /**
237      * Loads and processes package deny data from Mobile Data Download (MDD).
238      *
239      * <p>This method orchestrates the retrieval and processing of package deny data from MDD. It
240      * performs the following steps asynchronously: 1. **Checks Feature Flag:** Verifies if the
241      * package deny feature is enabled (`mEnablePackageDeny`). If disabled, it returns a {@link
242      * PackageDenyMddProcessStatus#DISABLED} status. 2. **Fetches MDD File Group:** Initiates a
243      * request to MDD to fetch the file group associated with the package deny data (using
244      * `mMobileDataDownload::getFileGroup`). 3. **Extracts MDD File:** Obtains the actual file from
245      * the fetched file group (using `this::getMddFile`). 4. **Parses the File:** Parses the content
246      * of the MDD file, converting it into a usable data structure (using `this::parseFile`). 5.
247      * **Filters Data:** Filters the parsed data to retain only information relevant to installed
248      * packages (using `this::filterMddDataToInstalledPackages`). 6. **Updates Cache:**
249      * Asynchronously updates the package deny cache data store (`mPackageDenyCacheDataStore`) with
250      * the processed data. 7. **Handles Errors:** Includes error handling for `PackageDenyException`
251      * and general `Exception` to log issues and return appropriate {@link
252      * PackageDenyMddProcessStatus} values.
253      *
254      * @return A {@link ListenableFuture} representing the eventual result of the MDD data loading
255      *     and processing operation. The future yields a {@link PackageDenyMddProcessStatus}
256      *     indicating the outcome of the process (success, failure, or disabled).
257      */
loadDenyDataFromMdd()258     public ListenableFuture<PackageDenyMddProcessStatus> loadDenyDataFromMdd() {
259         LogUtil.d("Executing load deny data from mdd download");
260         if (!mEnablePackageDeny) {
261             LogUtil.d("Package deny service flag is false, returning...");
262             PackageDenyMddProcessStatus.logError(PackageDenyMddProcessStatus.DISABLED);
263             return Futures.immediateFuture(PackageDenyMddProcessStatus.DISABLED);
264         }
265         return FluentFuture.from(
266                         Futures.immediateFuture(
267                                 GetFileGroupRequest.newBuilder().setGroupName(GROUP_NAME).build()))
268                 .transformAsync(
269                         mMobileDataDownload::getFileGroup,
270                         AdServicesExecutors.getLightWeightExecutor())
271                 .transform(this::getMddFile, AdServicesExecutors.getLightWeightExecutor())
272                 .transform(this::parseFile, AdServicesExecutors.getBackgroundExecutor())
273                 .transform(this::convertToCacheMap, AdServicesExecutors.getLightWeightExecutor())
274                 .transformAsync(
275                         map -> mPackageDenyCacheDataStore.updateDataAsync(data -> map),
276                         AdServicesExecutors.getBackgroundExecutor())
277                 .transform(
278                         // TODO (b/365605754) add metrics for success
279                         x -> PackageDenyMddProcessStatus.SUCCESS,
280                         AdServicesExecutors.getLightWeightExecutor())
281                 .catching(
282                         PackageDenyException.class,
283                         e -> {
284                             LogUtil.e(
285                                     "PackageDenyException: %s with cause: %s",
286                                     e.getMessage(), e.getCause());
287                             PackageDenyMddProcessStatus packageDenyMddProcessStatus =
288                                     PackageDenyMddProcessStatus.valueOf(e.getMessage());
289                             PackageDenyMddProcessStatus.logError(packageDenyMddProcessStatus);
290                             return packageDenyMddProcessStatus;
291                         },
292                         AdServicesExecutors.getLightWeightExecutor())
293                 .catching(
294                         Exception.class,
295                         e -> {
296                             LogUtil.e("Failure in deny package mdd process %s", e.getMessage());
297                             PackageDenyMddProcessStatus.logError(
298                                     PackageDenyMddProcessStatus.FAILED_UPDATING_CACHE);
299                             return PackageDenyMddProcessStatus.FAILED_UPDATING_CACHE;
300                         },
301                         AdServicesExecutors.getLightWeightExecutor());
302     }
303 
getMddFile( ClientConfigProto.ClientFileGroup clientFileGroup)304     private ClientConfigProto.ClientFile getMddFile(
305             ClientConfigProto.ClientFileGroup clientFileGroup) {
306         if (clientFileGroup == null) {
307             LogUtil.d("MDD has not downloaded the package deny file yet for group %s", GROUP_NAME);
308             throw new PackageDenyException(PackageDenyMddProcessStatus.NO_FILE_FOUND);
309         }
310         return clientFileGroup.getFileList().stream()
311                 .filter(file -> file.getFileId().endsWith(".pb"))
312                 .findFirst()
313                 .orElseThrow(
314                         () -> new PackageDenyException(PackageDenyMddProcessStatus.NO_FILE_FOUND));
315     }
316 
parseFile(ClientConfigProto.ClientFile clientFile)317     private PackageToApiDenyGroupsMap parseFile(ClientConfigProto.ClientFile clientFile) {
318         try {
319             return PackageToApiDenyGroupsMap.parseFrom(
320                     mFileStorage.open(
321                             Uri.parse(clientFile.getFileUri()), ReadStreamOpener.create()));
322         } catch (IOException e) {
323             throw new PackageDenyException(PackageDenyMddProcessStatus.FAILED_READING_FILE, e);
324         }
325     }
326 
327     /**
328      * Converts the PackageToApiDenyGroupsMap to PackageToApiDenyGroupsCacheMap based on whether the
329      * package is installed.
330      *
331      * <p>If the enableInstalledPackageFilter flag is enabled, this method retrieves a map of
332      * installed packages and their version codes, and then filters the input
333      * packageToApiDenyGroupsMap to only include entries for installed packages. If the flag is
334      * disabled, the input map is returned without filtering and does not deny based on package
335      * version.
336      *
337      * @param packageToApiDenyGroupsMap The input PackageToApiDenyGroupsMap to filter.
338      * @return The converted PackageToApiDenyGroupsCacheMap.
339      * @throws PackageDenyException if an error occurs during the filtering process, specifically
340      *     with PackageDenyMddProcessStatus#FAILED_FILTERING_INSTALLED_PACKAGES status.
341      */
342     @VisibleForTesting
convertToCacheMap( PackageToApiDenyGroupsMap packageToApiDenyGroupsMap)343     PackageToApiDenyGroupsCacheMap convertToCacheMap(
344             PackageToApiDenyGroupsMap packageToApiDenyGroupsMap) {
345         try {
346             if (FlagsFactory.getFlags().getPackageDenyEnableInstalledPackageFilter()) {
347                 Map<String, Long> installedPackages =
348                         getInstalledPackageNameToVersionCodeMap(
349                                 ApplicationContextSingleton.get().getPackageManager());
350                 return getCacheMapFilteredByInstalledPackages(
351                         packageToApiDenyGroupsMap, installedPackages);
352             } else {
353                 LogUtil.d("Package installed filter is disabled");
354                 return getCacheMapWithoutFilter(packageToApiDenyGroupsMap);
355             }
356 
357         } catch (Exception e) {
358             throw new PackageDenyException(
359                     PackageDenyMddProcessStatus.FAILED_FILTERING_INSTALLED_PACKAGES, e);
360         }
361     }
362 
363     /**
364      * Filters a PackageToApiDenyGroupsMap to include only installed packages and transforms it into
365      * a PackageToApiDenyGroupsCacheMap with optimized API group representations.
366      *
367      * <p>This method takes a PackageToApiDenyGroupsMap and a map of installed packages with their
368      * version codes. It performs the following steps: 1. **Filters the input map:** Keeps only the
369      * entries corresponding to installed packages. 2. **Groups by package type:** Groups the
370      * entries into two categories: APP and SDK. 3. **Transforms into nested maps:** Creates a
371      * nested map structure where the outer map keys are package types (`PackageType`), and the
372      * inner map keys are package names with values as lists of applicable API groups. This is done
373      * by calling `getApiGroupsForInstalledPackages` to determine the relevant API groups for each
374      * package based on its installed version. 4. Converts the inner maps from "package name to list
375      * of API groups to "package name to `ApiGroupsCache`" using the `mapToApiGroupsCache` method.
376      * 5. **Constructs the result:** Builds a `PackageToApiDenyGroupsCacheMap` object from the
377      * optimized map, separating APP and SDK entries into their respective fields.
378      *
379      * @param packageToApiDenyGroupsMap The input PackageToApiDenyGroupsMap to be filtered and
380      *     transformed.
381      * @param installedPackages A map of installed package names to their corresponding version
382      *     codes.
383      * @return A PackageToApiDenyGroupsCacheMap containing filtered and optimized API deny group
384      *     information for installed packages.
385      */
getCacheMapFilteredByInstalledPackages( PackageToApiDenyGroupsMap packageToApiDenyGroupsMap, Map<String, Long> installedPackages)386     private static PackageToApiDenyGroupsCacheMap getCacheMapFilteredByInstalledPackages(
387             PackageToApiDenyGroupsMap packageToApiDenyGroupsMap,
388             Map<String, Long> installedPackages) {
389         Map<PackageType, Map<String, List<String>>> map =
390                 packageToApiDenyGroupsMap.getMapMap().entrySet().stream()
391                         // filter installed packages from the file
392                         .filter(
393                                 packageDenyApiGroupsEntry ->
394                                         installedPackages.containsKey(
395                                                 packageDenyApiGroupsEntry.getKey()))
396                         .collect(
397                                 Collectors.groupingBy(
398                                         entry -> entry.getValue().getPackageType(),
399                                         Collectors.mapping(
400                                                 entry -> entry,
401                                                 Collectors.toMap(
402                                                         Map.Entry::getKey,
403                                                         e ->
404                                                                 getApiGroupsForInstalledPackages(
405                                                                         e.getValue(),
406                                                                         installedPackages.get(
407                                                                                 e.getKey()))))));
408 
409         // TODO (b/365605754) add error log for unknown package type
410         return getPackageToApiDenyGroupsCacheMap(map);
411     }
412 
getPackageToApiDenyGroupsCacheMap( Map<PackageType, Map<String, List<String>>> map)413     private static PackageToApiDenyGroupsCacheMap getPackageToApiDenyGroupsCacheMap(
414             Map<PackageType, Map<String, List<String>>> map) {
415         LogUtil.d("package deny map for cache is %s", map);
416         Map<PackageType, Map<String, ApiGroupsCache>> apiGroupsCacheMap =
417                 map.entrySet().stream()
418                         .map((e -> Map.entry(e.getKey(), mapToApiGroupsCache(e.getValue()))))
419                         .filter(e -> e.getValue().size() > 0)
420                         .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
421         return PackageToApiDenyGroupsCacheMap.newBuilder()
422                 .putAllAppToApiDenyGroupsCacheMap(
423                         apiGroupsCacheMap.getOrDefault(PackageType.APP, Collections.emptyMap()))
424                 .putAllSdkToApiDenyGroupsCacheMap(
425                         apiGroupsCacheMap.getOrDefault(PackageType.SDK, Collections.emptyMap()))
426                 .build();
427     }
428 
getCacheMapWithoutFilter( PackageToApiDenyGroupsMap packageToApiDenyGroupsMap)429     private static PackageToApiDenyGroupsCacheMap getCacheMapWithoutFilter(
430             PackageToApiDenyGroupsMap packageToApiDenyGroupsMap) {
431         Map<PackageType, Map<String, List<String>>> map =
432                 packageToApiDenyGroupsMap.getMapMap().entrySet().stream()
433                         .collect(
434                                 Collectors.groupingBy(
435                                         entry -> entry.getValue().getPackageType(),
436                                         Collectors.mapping(
437                                                 entry -> entry,
438                                                 Collectors.toMap(
439                                                         Map.Entry::getKey,
440                                                         e -> collectApiGroups(e.getValue())))));
441         return getPackageToApiDenyGroupsCacheMap(map);
442     }
443 
collectApiGroups(ApiDenyGroupsForPackage apiDenyGroupsForPackage)444     private static List<String> collectApiGroups(ApiDenyGroupsForPackage apiDenyGroupsForPackage) {
445         return apiDenyGroupsForPackage.getApiDenyGroupsForPackageVersionsList().stream()
446                 .flatMap(
447                         appApiDenyGroup ->
448                                 appApiDenyGroup.getApiGroups().getApiGroupList().stream())
449                 .distinct()
450                 .collect(Collectors.toList());
451     }
452 
453     /**
454      * Converts a map of package names to lists of API groups into a map of package names to
455      * ApiGroupsCache objects.
456      *
457      * <p>This method iterates through the input map, filters out entries with empty API group
458      * lists, and then transforms each remaining entry into a new entry where the value is an
459      * ApiGroupsCache object built from the original list of API groups. The resulting map provides
460      * an optimized representation of API group information for each package.
461      *
462      * @param map The input map where keys are package names and values are API group names.
463      * @return A map where keys are package names and values are corresponding ApiGroupsCache
464      *     objects.
465      */
mapToApiGroupsCache(Map<String, List<String>> map)466     private static Map<String, ApiGroupsCache> mapToApiGroupsCache(Map<String, List<String>> map) {
467         return map.entrySet().stream()
468                 .filter(e1 -> e1.getValue().size() > 0)
469                 .map(
470                         e1 ->
471                                 Map.entry(
472                                         e1.getKey(),
473                                         ApiGroupsCache.newBuilder()
474                                                 .addAllApiGroup(e1.getValue())
475                                                 .build()))
476                 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
477     }
478 
479     /**
480      * Gets a map of installed package names to their corresponding version codes.
481      *
482      * @param pm The {@link PackageManager} to use for retrieving installed applications.
483      * @return A map where the keys are package names (String) and the values are version codes
484      *     (Long).
485      */
getInstalledPackageNameToVersionCodeMap(PackageManager pm)486     private Map<String, Long> getInstalledPackageNameToVersionCodeMap(PackageManager pm) {
487         Map<String, Long> installedPackages =
488                 pm.getInstalledApplications(PackageManager.GET_META_DATA).stream()
489                         .filter(applicationInfo -> applicationInfo.packageName != null)
490                         .collect(
491                                 Collectors.groupingBy(
492                                         applicationInfo -> applicationInfo.packageName,
493                                         Collectors.reducing(
494                                                 0L, // Initial value
495                                                 applicationInfo ->
496                                                         getPackageVersion(pm, applicationInfo),
497                                                 // Map each application to their versioncode
498                                                 (v1, v2) -> v1 == 0L ? v2 : v1
499                                                 // Keep the first version code encountered
500                                                 )));
501         LogUtil.d("installed packages name to versioncode map is %s", installedPackages);
502         return installedPackages;
503     }
504 
505     /**
506      * Retrieves a list of API groups that apply to an installed package based on its version.
507      *
508      * <p>This method iterates through the `apiDenyGroupsForPackageVersionsList` in the provided
509      * ApiDenyGroupsForPackage object. It filters the list to include only those version ranges that
510      * encompass the given `installedVersion`. For each matching version range, it extracts the
511      * associated API groups, flattens them into a single stream, removes duplicates, and finally
512      * collects them into a list.
513      *
514      * @param apiDenyGroupsForPackage The ApiDenyGroupsForPackage object containing the API deny
515      *     groups and their corresponding package version ranges.
516      * @param installedVersion The installed version code of the package.
517      * @return A list of distinct API group names (String) that apply to the installed package
518      *     version.
519      */
getApiGroupsForInstalledPackages( ApiDenyGroupsForPackage apiDenyGroupsForPackage, long installedVersion)520     private static List<String> getApiGroupsForInstalledPackages(
521             ApiDenyGroupsForPackage apiDenyGroupsForPackage, long installedVersion) {
522         return apiDenyGroupsForPackage.getApiDenyGroupsForPackageVersionsList().stream()
523                 .filter(
524                         apiDenyGroupsForAppVersions ->
525                                 installedVersion
526                                                 >= apiDenyGroupsForAppVersions
527                                                         .getPackageMinVersion()
528                                         && installedVersion
529                                                 <= (apiDenyGroupsForAppVersions
530                                                                         .getPackageMaxVersion()
531                                                                 > 0
532                                                         ? apiDenyGroupsForAppVersions
533                                                                 .getPackageMaxVersion()
534                                                         : Long.MAX_VALUE))
535                 .flatMap(
536                         appApiDenyGroup ->
537                                 appApiDenyGroup.getApiGroups().getApiGroupList().stream())
538                 .distinct()
539                 .collect(Collectors.toList());
540     }
541 
542     /**
543      * Retrieves the version code of a package.
544      *
545      * <p>This method attempts to retrieve the long version code of a package using the provided
546      * {@link PackageManager} and {@link ApplicationInfo}. If the package is not found, it logs an
547      * error and returns 0.
548      *
549      * @param pm The {@link PackageManager} to use for retrieving package information.
550      * @param applicationInfo The {@link ApplicationInfo} containing the package name.
551      * @return The long version code of the package, or 0 if the package is not found.
552      */
getPackageVersion(PackageManager pm, ApplicationInfo applicationInfo)553     private static long getPackageVersion(PackageManager pm, ApplicationInfo applicationInfo) {
554         try {
555             return pm.getPackageInfo(applicationInfo.packageName, 0).getLongVersionCode();
556         } catch (PackageManager.NameNotFoundException e) {
557             LogUtil.e(
558                     "Deny package service cannot find package version for %s",
559                     applicationInfo.packageName);
560             return 0L;
561         }
562     }
563 
564     public enum PackageDenyMddProcessStatus {
565         SUCCESS(-1),
566         DISABLED(AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PACKAGE_DENY_PROCESS_ERROR_DISABLED),
567         FAILURE(AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PACKAGE_DENY_PROCESS_ERROR_FAILURE_UNKNOWN),
568         NO_FILE_FOUND(
569                 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PACKAGE_DENY_PROCESS_ERROR_NO_FILE_FOUND),
570         FAILED_READING_FILE(
571                 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PACKAGE_DENY_PROCESS_ERROR_FAILED_READING_FILE),
572         FAILED_FILTERING_INSTALLED_PACKAGES(
573                 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PACKAGE_DENY_PROCESS_ERROR_FAILED_FILTERING_INSTALLED_PACKAGES),
574         FAILED_UPDATING_CACHE(
575                 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PACKAGE_DENY_PROCESS_ERROR_FAILED_UPDATING_CACHE),
576         FAILED_READING_CACHE(
577                 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PACKAGE_DENY_PROCESS_ERROR_FAILED_READING_CACHE),
578         ;
579 
580         private final int mAdServicesErrorReportedErrorCodePackageDenyProcessError;
581 
PackageDenyMddProcessStatus(int adServicesErrorReportedErrorCodePackageDenyProcessError)582         PackageDenyMddProcessStatus(int adServicesErrorReportedErrorCodePackageDenyProcessError) {
583             this.mAdServicesErrorReportedErrorCodePackageDenyProcessError =
584                     adServicesErrorReportedErrorCodePackageDenyProcessError;
585         }
586 
logError(PackageDenyMddProcessStatus packageDenyMddProcessStatus)587         static void logError(PackageDenyMddProcessStatus packageDenyMddProcessStatus) {
588             ErrorLogUtil.e(
589                     packageDenyMddProcessStatus
590                             .mAdServicesErrorReportedErrorCodePackageDenyProcessError,
591                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON);
592         }
593     }
594 
595     private static final class PackageDenyException extends RuntimeException {
PackageDenyException(PackageDenyMddProcessStatus packageDenyMddProcessStatus)596         PackageDenyException(PackageDenyMddProcessStatus packageDenyMddProcessStatus) {
597             super(packageDenyMddProcessStatus.name());
598         }
599 
PackageDenyException(PackageDenyMddProcessStatus packageDenyMddProcessStatus, Throwable t)600         PackageDenyException(PackageDenyMddProcessStatus packageDenyMddProcessStatus, Throwable t) {
601             super(packageDenyMddProcessStatus.name(), t);
602         }
603     }
604 }
605