1 /* 2 * Copyright 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.external.localstorage; 18 19 import android.app.appsearch.exceptions.AppSearchException; 20 import android.util.ArrayMap; 21 import android.util.ArraySet; 22 23 import com.android.appsearch.flags.Flags; 24 import com.android.internal.annotations.VisibleForTesting; 25 import com.android.server.appsearch.external.localstorage.util.PrefixUtil; 26 27 import com.google.android.icing.proto.SchemaTypeConfigProto; 28 29 import org.jspecify.annotations.NonNull; 30 31 import java.util.ArrayDeque; 32 import java.util.ArrayList; 33 import java.util.Collections; 34 import java.util.List; 35 import java.util.Map; 36 import java.util.Objects; 37 import java.util.Queue; 38 import java.util.Set; 39 40 /** 41 * Caches and manages schema information for AppSearch. 42 * 43 * @hide 44 */ 45 public class SchemaCache { 46 /** 47 * A map that contains schema types and SchemaTypeConfigProtos for all package-database 48 * prefixes. It maps each package-database prefix to an inner-map. The inner-map maps each 49 * prefixed schema type to its respective SchemaTypeConfigProto. 50 */ 51 private final Map<String, Map<String, SchemaTypeConfigProto>> mSchemaMap = new ArrayMap<>(); 52 53 /** 54 * A map that contains schema types and all children schema types for all package-database 55 * prefixes. It maps each package-database prefix to an inner-map. The inner-map maps each 56 * prefixed schema type to its respective list of children prefixed schema types. 57 */ 58 private final Map<String, Map<String, List<String>>> mSchemaParentToChildrenMap = 59 new ArrayMap<>(); 60 61 /** 62 * A map that contains schema types and all parent schema types for all package-database 63 * prefixes. It maps each package-database prefix to an inner-map. The inner-map maps each 64 * prefixed schema type to its respective list of unprefixed parent schema types including 65 * transitive parents. It's guaranteed that child types always appear before parent types in the 66 * list. 67 */ 68 private final Map<String, Map<String, List<String>>> 69 mSchemaChildToTransitiveUnprefixedParentsMap = new ArrayMap<>(); 70 SchemaCache()71 public SchemaCache() {} 72 73 @VisibleForTesting SchemaCache(@onNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap)74 public SchemaCache(@NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) 75 throws AppSearchException { 76 mSchemaMap.putAll(Objects.requireNonNull(schemaMap)); 77 rebuildCache(); 78 } 79 80 /** Returns the schema map for the given prefix. */ getSchemaMapForPrefix( @onNull String prefix)81 public @NonNull Map<String, SchemaTypeConfigProto> getSchemaMapForPrefix( 82 @NonNull String prefix) { 83 Objects.requireNonNull(prefix); 84 85 Map<String, SchemaTypeConfigProto> schemaMap = mSchemaMap.get(prefix); 86 if (schemaMap == null) { 87 return Collections.emptyMap(); 88 } 89 return schemaMap; 90 } 91 92 /** Returns a set of all prefixes stored in the cache. */ getAllPrefixes()93 public @NonNull Set<String> getAllPrefixes() { 94 return Collections.unmodifiableSet(mSchemaMap.keySet()); 95 } 96 97 /** 98 * Returns all prefixed schema types stored in the cache. 99 * 100 * <p>This method is inefficient to call repeatedly. 101 */ getAllPrefixedSchemaTypes()102 public @NonNull List<String> getAllPrefixedSchemaTypes() { 103 List<String> cachedPrefixedSchemaTypes = new ArrayList<>(); 104 for (Map<String, SchemaTypeConfigProto> value : mSchemaMap.values()) { 105 cachedPrefixedSchemaTypes.addAll(value.keySet()); 106 } 107 return cachedPrefixedSchemaTypes; 108 } 109 110 /** 111 * Returns the schema types for the given set of prefixed schema types with their descendants, 112 * based on the schema parent-to-children map held in the cache. 113 */ getSchemaTypesWithDescendants( @onNull String prefix, @NonNull Set<String> prefixedSchemaTypes)114 public @NonNull Set<String> getSchemaTypesWithDescendants( 115 @NonNull String prefix, @NonNull Set<String> prefixedSchemaTypes) { 116 Objects.requireNonNull(prefix); 117 Objects.requireNonNull(prefixedSchemaTypes); 118 Map<String, List<String>> parentToChildrenMap = mSchemaParentToChildrenMap.get(prefix); 119 if (parentToChildrenMap == null) { 120 parentToChildrenMap = Collections.emptyMap(); 121 } 122 123 // Perform a BFS search on the inheritance graph started by the set of prefixedSchemaTypes. 124 Set<String> visited = new ArraySet<>(); 125 Queue<String> prefixedSchemaQueue = new ArrayDeque<>(prefixedSchemaTypes); 126 while (!prefixedSchemaQueue.isEmpty()) { 127 String currentPrefixedSchema = prefixedSchemaQueue.poll(); 128 if (visited.contains(currentPrefixedSchema)) { 129 continue; 130 } 131 visited.add(currentPrefixedSchema); 132 List<String> children = parentToChildrenMap.get(currentPrefixedSchema); 133 if (children == null) { 134 continue; 135 } 136 prefixedSchemaQueue.addAll(children); 137 } 138 139 return visited; 140 } 141 142 /** 143 * Returns the unprefixed parent schema types, including transitive parents, for the given 144 * prefixed schema type, based on the schema child-to-parents map held in the cache. It's 145 * guaranteed that child types always appear before parent types in the list. 146 */ getTransitiveUnprefixedParentSchemaTypes( @onNull String prefix, @NonNull String prefixedSchemaType)147 public @NonNull List<String> getTransitiveUnprefixedParentSchemaTypes( 148 @NonNull String prefix, @NonNull String prefixedSchemaType) throws AppSearchException { 149 Objects.requireNonNull(prefix); 150 Objects.requireNonNull(prefixedSchemaType); 151 152 // If the flag is on, retrieve the parent types from the cache as it is available. 153 // Otherwise, recalculate the parent types. 154 if (Flags.enableSearchResultParentTypes()) { 155 Map<String, List<String>> unprefixedChildToParentsMap = 156 mSchemaChildToTransitiveUnprefixedParentsMap.get(prefix); 157 if (unprefixedChildToParentsMap == null) { 158 return Collections.emptyList(); 159 } 160 List<String> parents = unprefixedChildToParentsMap.get(prefixedSchemaType); 161 return parents == null ? Collections.emptyList() : parents; 162 } else { 163 return calculateTransitiveUnprefixedParentSchemaTypes( 164 prefixedSchemaType, getSchemaMapForPrefix(prefix)); 165 } 166 } 167 168 /** 169 * Rebuilds the schema parent-to-children and child-to-parents maps for the given prefix, based 170 * on the current schema map. 171 * 172 * <p>The schema parent-to-children and child-to-parents maps must be updated when {@link 173 * #addToSchemaMap} or {@link #removeFromSchemaMap} has been called. Otherwise, the results from 174 * {@link #getSchemaTypesWithDescendants} and {@link #getTransitiveUnprefixedParentSchemaTypes} 175 * would be stale. 176 */ rebuildCacheForPrefix(@onNull String prefix)177 public void rebuildCacheForPrefix(@NonNull String prefix) throws AppSearchException { 178 Objects.requireNonNull(prefix); 179 180 mSchemaParentToChildrenMap.remove(prefix); 181 mSchemaChildToTransitiveUnprefixedParentsMap.remove(prefix); 182 Map<String, SchemaTypeConfigProto> prefixedSchemaMap = mSchemaMap.get(prefix); 183 if (prefixedSchemaMap == null) { 184 return; 185 } 186 187 // Build the parent-to-children map for the current prefix. 188 Map<String, List<String>> parentToChildrenMap = new ArrayMap<>(); 189 for (SchemaTypeConfigProto childSchemaConfig : prefixedSchemaMap.values()) { 190 for (int i = 0; i < childSchemaConfig.getParentTypesCount(); i++) { 191 String parent = childSchemaConfig.getParentTypes(i); 192 List<String> children = parentToChildrenMap.get(parent); 193 if (children == null) { 194 children = new ArrayList<>(); 195 parentToChildrenMap.put(parent, children); 196 } 197 children.add(childSchemaConfig.getSchemaType()); 198 } 199 } 200 // Record the map for the current prefix. 201 if (!parentToChildrenMap.isEmpty()) { 202 mSchemaParentToChildrenMap.put(prefix, parentToChildrenMap); 203 } 204 205 // If the flag is on, build the child-to-parent maps as caches. Otherwise, this 206 // information will have to be recalculated when needed. 207 if (Flags.enableSearchResultParentTypes()) { 208 // Build the child-to-parents maps for the current prefix. 209 Map<String, List<String>> childToTransitiveUnprefixedParentsMap = new ArrayMap<>(); 210 for (SchemaTypeConfigProto childSchemaConfig : prefixedSchemaMap.values()) { 211 if (childSchemaConfig.getParentTypesCount() > 0) { 212 childToTransitiveUnprefixedParentsMap.put( 213 childSchemaConfig.getSchemaType(), 214 calculateTransitiveUnprefixedParentSchemaTypes( 215 childSchemaConfig.getSchemaType(), prefixedSchemaMap)); 216 } 217 } 218 // Record the map for the current prefix. 219 if (!childToTransitiveUnprefixedParentsMap.isEmpty()) { 220 mSchemaChildToTransitiveUnprefixedParentsMap.put( 221 prefix, childToTransitiveUnprefixedParentsMap); 222 } 223 } 224 } 225 226 /** 227 * Rebuilds the schema parent-to-children and child-to-parents maps based on the current schema 228 * map. 229 * 230 * <p>The schema parent-to-children and child-to-parents maps must be updated when {@link 231 * #addToSchemaMap} or {@link #removeFromSchemaMap} has been called. Otherwise, the results from 232 * {@link #getSchemaTypesWithDescendants} and {@link #getTransitiveUnprefixedParentSchemaTypes} 233 * would be stale. 234 */ rebuildCache()235 public void rebuildCache() throws AppSearchException { 236 mSchemaParentToChildrenMap.clear(); 237 mSchemaChildToTransitiveUnprefixedParentsMap.clear(); 238 for (String prefix : mSchemaMap.keySet()) { 239 rebuildCacheForPrefix(prefix); 240 } 241 } 242 243 /** 244 * Adds a schema to the schema map. 245 * 246 * <p>Note that this method will invalidate the schema parent-to-children and child-to-parents 247 * maps in the cache, and either {@link #rebuildCache} or {@link #rebuildCacheForPrefix} is 248 * required to be called to update the cache. 249 */ addToSchemaMap( @onNull String prefix, @NonNull SchemaTypeConfigProto schemaTypeConfigProto)250 public void addToSchemaMap( 251 @NonNull String prefix, @NonNull SchemaTypeConfigProto schemaTypeConfigProto) { 252 Objects.requireNonNull(prefix); 253 Objects.requireNonNull(schemaTypeConfigProto); 254 255 Map<String, SchemaTypeConfigProto> schemaTypeMap = mSchemaMap.get(prefix); 256 if (schemaTypeMap == null) { 257 schemaTypeMap = new ArrayMap<>(); 258 mSchemaMap.put(prefix, schemaTypeMap); 259 } 260 schemaTypeMap.put(schemaTypeConfigProto.getSchemaType(), schemaTypeConfigProto); 261 } 262 263 /** 264 * Removes a schema from the schema map. 265 * 266 * <p>Note that this method will invalidate the schema parent-to-children and child-to-parents 267 * maps in the cache, and either {@link #rebuildCache} or {@link #rebuildCacheForPrefix} is 268 * required to be called to update the cache. 269 */ removeFromSchemaMap(@onNull String prefix, @NonNull String schemaType)270 public void removeFromSchemaMap(@NonNull String prefix, @NonNull String schemaType) { 271 Objects.requireNonNull(prefix); 272 Objects.requireNonNull(schemaType); 273 274 Map<String, SchemaTypeConfigProto> schemaTypeMap = mSchemaMap.get(prefix); 275 if (schemaTypeMap != null) { 276 schemaTypeMap.remove(schemaType); 277 } 278 } 279 280 /** 281 * Removes the entry of the given prefix from the schema map, the schema parent-to-children map 282 * and the child-to-parents map, and returns the set of removed prefixed schema type. 283 */ removePrefix(@onNull String prefix)284 public @NonNull Set<String> removePrefix(@NonNull String prefix) { 285 Objects.requireNonNull(prefix); 286 287 Map<String, SchemaTypeConfigProto> removedSchemas = 288 Objects.requireNonNull(mSchemaMap.remove(prefix)); 289 mSchemaParentToChildrenMap.remove(prefix); 290 mSchemaChildToTransitiveUnprefixedParentsMap.remove(prefix); 291 return removedSchemas.keySet(); 292 } 293 294 /** Clears all data in the cache. */ clear()295 public void clear() { 296 mSchemaMap.clear(); 297 mSchemaParentToChildrenMap.clear(); 298 mSchemaChildToTransitiveUnprefixedParentsMap.clear(); 299 } 300 301 /** 302 * Get the list of unprefixed transitive parent type names of {@code prefixedSchemaType}. 303 * 304 * <p>It's guaranteed that child types always appear before parent types in the list. 305 */ calculateTransitiveUnprefixedParentSchemaTypes( @onNull String prefixedSchemaType, @NonNull Map<String, SchemaTypeConfigProto> prefixedSchemaMap)306 private @NonNull List<String> calculateTransitiveUnprefixedParentSchemaTypes( 307 @NonNull String prefixedSchemaType, 308 @NonNull Map<String, SchemaTypeConfigProto> prefixedSchemaMap) 309 throws AppSearchException { 310 // Please note that neither DFS nor BFS order is guaranteed to always put child types 311 // before parent types (due to the diamond problem), so a topological sorting algorithm 312 // is required. 313 Map<String, Integer> inDegreeMap = new ArrayMap<>(); 314 collectParentTypeInDegrees( 315 prefixedSchemaType, 316 prefixedSchemaMap, 317 /* visited= */ new ArraySet<>(), 318 inDegreeMap); 319 320 List<String> result = new ArrayList<>(); 321 Queue<String> queue = new ArrayDeque<>(); 322 // prefixedSchemaType is the only type that has zero in-degree at this point. 323 queue.add(prefixedSchemaType); 324 while (!queue.isEmpty()) { 325 SchemaTypeConfigProto currentSchema = 326 Objects.requireNonNull(prefixedSchemaMap.get(queue.poll())); 327 for (int i = 0; i < currentSchema.getParentTypesCount(); ++i) { 328 String prefixedParentType = currentSchema.getParentTypes(i); 329 int parentInDegree = 330 Objects.requireNonNull(inDegreeMap.get(prefixedParentType)) - 1; 331 inDegreeMap.put(prefixedParentType, parentInDegree); 332 if (parentInDegree == 0) { 333 result.add(PrefixUtil.removePrefix(prefixedParentType)); 334 queue.add(prefixedParentType); 335 } 336 } 337 } 338 return result; 339 } 340 collectParentTypeInDegrees( @onNull String prefixedSchemaType, @NonNull Map<String, SchemaTypeConfigProto> schemaTypeMap, @NonNull Set<String> visited, @NonNull Map<String, Integer> inDegreeMap)341 private void collectParentTypeInDegrees( 342 @NonNull String prefixedSchemaType, 343 @NonNull Map<String, SchemaTypeConfigProto> schemaTypeMap, 344 @NonNull Set<String> visited, 345 @NonNull Map<String, Integer> inDegreeMap) { 346 if (visited.contains(prefixedSchemaType)) { 347 return; 348 } 349 visited.add(prefixedSchemaType); 350 SchemaTypeConfigProto schema = 351 Objects.requireNonNull(schemaTypeMap.get(prefixedSchemaType)); 352 for (int i = 0; i < schema.getParentTypesCount(); ++i) { 353 String prefixedParentType = schema.getParentTypes(i); 354 Integer parentInDegree = inDegreeMap.get(prefixedParentType); 355 if (parentInDegree == null) { 356 parentInDegree = 0; 357 } 358 inDegreeMap.put(prefixedParentType, parentInDegree + 1); 359 collectParentTypeInDegrees(prefixedParentType, schemaTypeMap, visited, inDegreeMap); 360 } 361 } 362 } 363