• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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