1 /*
2  * Copyright (C) 2022 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.adservices.service.measurement.util;
18 
19 import android.annotation.NonNull;
20 
21 import com.android.adservices.service.Flags;
22 import com.android.adservices.service.measurement.FilterMap;
23 import com.android.adservices.service.measurement.FilterValue;
24 
25 import org.json.JSONArray;
26 import org.json.JSONException;
27 import org.json.JSONObject;
28 
29 import java.util.ArrayList;
30 import java.util.HashSet;
31 import java.util.List;
32 import java.util.Set;
33 
34 /** Filtering utilities for measurement. */
35 public final class Filter {
36     private final Flags mFlags;
37 
Filter(@onNull Flags flags)38     public Filter(@NonNull Flags flags) {
39         mFlags = flags;
40     }
41 
42     /**
43      * Checks whether source filter and trigger filter are matched. When a key is only present in
44      * source or trigger, ignore that key. When a key is present both in source and trigger, the key
45      * matches if the intersection of values is not empty.
46      *
47      * @param sourceFilter the {@code FilterMap} in attribution source.
48      * @param triggerFilters a list of {@code FilterMap}, the trigger filter set.
49      * @param isFilter true for filters, false for not_filters.
50      * @return return true when all keys shared by source filter and trigger filter are matched.
51      */
isFilterMatch( FilterMap sourceFilter, List<FilterMap> triggerFilters, boolean isFilter)52     public boolean isFilterMatch(
53             FilterMap sourceFilter, List<FilterMap> triggerFilters, boolean isFilter) {
54         if (sourceFilter.isEmpty(mFlags) || triggerFilters.isEmpty()) {
55             return true;
56         }
57         for (FilterMap filterMap : triggerFilters) {
58             if (isFilterMatch(sourceFilter, filterMap, isFilter)) {
59                 return true;
60             }
61         }
62         return false;
63     }
64 
isFilterMatch( FilterMap sourceFilter, FilterMap triggerFilter, boolean isFilter)65     private boolean isFilterMatch(
66             FilterMap sourceFilter, FilterMap triggerFilter, boolean isFilter) {
67         return mFlags.getMeasurementEnableLookbackWindowFilter()
68                 ? isFilterMatchWithLookbackWindow(sourceFilter, triggerFilter, isFilter)
69                 : isFilterMatchV1(sourceFilter, triggerFilter, isFilter);
70     }
71 
isFilterMatchV1( FilterMap sourceFilter, FilterMap triggerFilter, boolean isFilter)72     private boolean isFilterMatchV1(
73             FilterMap sourceFilter, FilterMap triggerFilter, boolean isFilter) {
74         for (String key : triggerFilter.getAttributionFilterMap().keySet()) {
75             if (!sourceFilter.getAttributionFilterMap().containsKey(key)) {
76                 continue;
77             }
78             // Finds the intersection of two value lists.
79             List<String> sourceValues = sourceFilter.getAttributionFilterMap().get(key);
80             List<String> triggerValues = triggerFilter.getAttributionFilterMap().get(key);
81             if (!matchFilterValues(sourceValues, triggerValues, isFilter)) {
82                 return false;
83             }
84         }
85         return true;
86     }
87 
isFilterMatchWithLookbackWindow( FilterMap sourceFilter, FilterMap triggerFilter, boolean isFilter)88     private boolean isFilterMatchWithLookbackWindow(
89             FilterMap sourceFilter, FilterMap triggerFilter, boolean isFilter) {
90         for (String key : triggerFilter.getAttributionFilterMapWithLongValue().keySet()) {
91             if (!sourceFilter.getAttributionFilterMapWithLongValue().containsKey(key)) {
92                 continue;
93             }
94             FilterValue filterValue = triggerFilter.getAttributionFilterMapWithLongValue().get(key);
95             switch (filterValue.kind()) {
96                 case STRING_LIST_VALUE:
97                     // Finds the intersection of two value lists.
98                     List<String> sourceValues =
99                             sourceFilter
100                                     .getAttributionFilterMapWithLongValue()
101                                     .get(key)
102                                     .stringListValue();
103                     List<String> triggerValues =
104                             triggerFilter
105                                     .getAttributionFilterMapWithLongValue()
106                                     .get(key)
107                                     .stringListValue();
108                     if (!matchFilterValues(sourceValues, triggerValues, isFilter)) {
109                         return false;
110                     }
111                     break;
112                 case LONG_VALUE:
113                     if (!sourceFilter.getAttributionFilterMapWithLongValue().containsKey(key)
114                             || !FilterMap.LOOKBACK_WINDOW.equals(key)) {
115                         continue;
116                     }
117                     long lookbackWindow = triggerFilter.getLongValue(key);
118                     long durationFromSource = sourceFilter.getLongValue(key);
119                     boolean lessOrEqual = durationFromSource <= lookbackWindow;
120                     if (lessOrEqual != isFilter) {
121                         return false;
122                     }
123                     break;
124                 default:
125                     break;
126             }
127         }
128         return true;
129     }
130 
matchFilterValues( List<String> sourceValues, List<String> triggerValues, boolean isFilter)131     private boolean matchFilterValues(
132             List<String> sourceValues, List<String> triggerValues, boolean isFilter) {
133         if (triggerValues.isEmpty()) {
134             return isFilter ? sourceValues.isEmpty() : !sourceValues.isEmpty();
135         }
136         Set<String> intersection = new HashSet<>(sourceValues);
137         intersection.retainAll(triggerValues);
138         return isFilter ? !intersection.isEmpty() : intersection.isEmpty();
139     }
140 
141     /**
142      * Deserializes the provided {@link JSONArray} of filters into filter set.
143      *
144      * @param filters serialized filter set
145      * @return deserialized filter set
146      * @throws JSONException if the deserialization fails
147      */
148     @NonNull
deserializeFilterSet(@onNull JSONArray filters)149     public List<FilterMap> deserializeFilterSet(@NonNull JSONArray filters) throws JSONException {
150         List<FilterMap> filterSet = new ArrayList<>();
151         for (int i = 0; i < filters.length(); i++) {
152             FilterMap filterMap =
153                     new FilterMap.Builder()
154                             .buildFilterData(filters.getJSONObject(i), mFlags)
155                             .build();
156             filterSet.add(filterMap);
157         }
158         return filterSet;
159     }
160 
161     /**
162      * Builds {@link JSONArray} our of the list of {@link List<FilterMap>} provided by serializing
163      * it recursively.
164      *
165      * @param filterMaps to be serialized
166      * @return serialized filter maps
167      */
168     @NonNull
serializeFilterSet(@onNull List<FilterMap> filterMaps)169     public JSONArray serializeFilterSet(@NonNull List<FilterMap> filterMaps) {
170         JSONArray serializedFilterMaps = new JSONArray();
171         for (FilterMap filter : filterMaps) {
172             serializedFilterMaps.put(filter.serializeAsJson(mFlags));
173         }
174         return serializedFilterMaps;
175     }
176 
177     /**
178      * Filters can be available in either {@link JSONObject} format or {@link JSONArray} format. For
179      * consistency across the board, this method wraps the {@link JSONObject} into {@link
180      * JSONArray}.
181      *
182      * @param json json where to look for the filter object
183      * @param key key with which the filter object is associated
184      * @return wrapped {@link JSONArray}
185      * @throws JSONException when creation of {@link JSONArray} fails
186      */
187     @NonNull
maybeWrapFilters(@onNull JSONObject json, @NonNull String key)188     public static JSONArray maybeWrapFilters(@NonNull JSONObject json, @NonNull String key)
189             throws JSONException {
190         JSONObject maybeFilterMap = json.optJSONObject(key);
191         if (maybeFilterMap != null) {
192             JSONArray filterSet = new JSONArray();
193             filterSet.put(maybeFilterMap);
194             return filterSet;
195         }
196         return json.getJSONArray(key);
197     }
198 }
199