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