• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2021 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.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.app.appsearch.observer.DocumentChangeInfo;
22 import android.app.appsearch.observer.ObserverCallback;
23 import android.app.appsearch.observer.ObserverSpec;
24 import android.app.appsearch.observer.SchemaChangeInfo;
25 import android.util.ArrayMap;
26 import android.util.ArraySet;
27 import android.util.Log;
28 
29 import com.android.internal.annotations.GuardedBy;
30 import com.android.server.appsearch.external.localstorage.util.PrefixUtil;
31 import com.android.server.appsearch.external.localstorage.visibilitystore.CallerAccess;
32 import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityChecker;
33 import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityStore;
34 import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityUtil;
35 
36 import java.util.ArrayList;
37 import java.util.Collections;
38 import java.util.List;
39 import java.util.Map;
40 import java.util.Objects;
41 import java.util.Set;
42 import java.util.concurrent.Executor;
43 
44 /**
45  * Manages {@link ObserverCallback} instances and queues notifications to them for later dispatch.
46  *
47  * <p>This class is thread-safe.
48  *
49  * @hide
50  */
51 public class ObserverManager {
52     private static final String TAG = "AppSearchObserverManage";
53 
54     /** The combination of fields by which {@link DocumentChangeInfo} is grouped. */
55     private static final class DocumentChangeGroupKey {
56         final String mPackageName;
57         final String mDatabaseName;
58         final String mNamespace;
59         final String mSchemaName;
60 
DocumentChangeGroupKey( @onNull String packageName, @NonNull String databaseName, @NonNull String namespace, @NonNull String schemaName)61         DocumentChangeGroupKey(
62                 @NonNull String packageName,
63                 @NonNull String databaseName,
64                 @NonNull String namespace,
65                 @NonNull String schemaName) {
66             mPackageName = Objects.requireNonNull(packageName);
67             mDatabaseName = Objects.requireNonNull(databaseName);
68             mNamespace = Objects.requireNonNull(namespace);
69             mSchemaName = Objects.requireNonNull(schemaName);
70         }
71 
72         @Override
equals(@ullable Object o)73         public boolean equals(@Nullable Object o) {
74             if (this == o) return true;
75             if (!(o instanceof DocumentChangeGroupKey)) return false;
76             DocumentChangeGroupKey that = (DocumentChangeGroupKey) o;
77             return mPackageName.equals(that.mPackageName)
78                     && mDatabaseName.equals(that.mDatabaseName)
79                     && mNamespace.equals(that.mNamespace)
80                     && mSchemaName.equals(that.mSchemaName);
81         }
82 
83         @Override
hashCode()84         public int hashCode() {
85             return Objects.hash(mPackageName, mDatabaseName, mNamespace, mSchemaName);
86         }
87     }
88 
89     private static final class ObserverInfo {
90         /** The package which registered the observer. */
91         final CallerAccess mListeningPackageAccess;
92 
93         final ObserverSpec mObserverSpec;
94         final Executor mExecutor;
95         final ObserverCallback mObserverCallback;
96         // Values is a set of document IDs
97         volatile Map<DocumentChangeGroupKey, Set<String>> mDocumentChanges = new ArrayMap<>();
98         // Keys are database prefixes, values are a set of schema names
99         volatile Map<String, Set<String>> mSchemaChanges = new ArrayMap<>();
100 
ObserverInfo( @onNull CallerAccess listeningPackageAccess, @NonNull ObserverSpec observerSpec, @NonNull Executor executor, @NonNull ObserverCallback observerCallback)101         ObserverInfo(
102                 @NonNull CallerAccess listeningPackageAccess,
103                 @NonNull ObserverSpec observerSpec,
104                 @NonNull Executor executor,
105                 @NonNull ObserverCallback observerCallback) {
106             mListeningPackageAccess = Objects.requireNonNull(listeningPackageAccess);
107             mObserverSpec = Objects.requireNonNull(observerSpec);
108             mExecutor = Objects.requireNonNull(executor);
109             mObserverCallback = Objects.requireNonNull(observerCallback);
110         }
111     }
112 
113     private final Object mLock = new Object();
114 
115     /** Maps target packages to ObserverInfos watching something in that package. */
116     @GuardedBy("mLock")
117     private final Map<String, List<ObserverInfo>> mObserversLocked = new ArrayMap<>();
118 
119     private volatile boolean mHasNotifications = false;
120 
121     /**
122      * Adds an {@link ObserverCallback} to monitor changes within the databases owned by {@code
123      * targetPackageName} if they match the given {@link
124      * android.app.appsearch.observer.ObserverSpec}.
125      *
126      * <p>If the data owned by {@code targetPackageName} is not visible to you, the registration
127      * call will succeed but no notifications will be dispatched. Notifications could start flowing
128      * later if {@code targetPackageName} changes its schema visibility settings.
129      *
130      * <p>If no package matching {@code targetPackageName} exists on the system, the registration
131      * call will succeed but no notifications will be dispatched. Notifications could start flowing
132      * later if {@code targetPackageName} is installed and starts indexing data.
133      *
134      * <p>Note that this method does not take the standard read/write lock that guards I/O, so it
135      * will not queue behind I/O. Therefore it is safe to call from any thread including UI or
136      * binder threads.
137      *
138      * @param listeningPackageAccess Visibility information about the app that wants to receive
139      *     notifications.
140      * @param targetPackageName The package that owns the data the observerCallback wants to be
141      *     notified for.
142      * @param spec Describes the kind of data changes the observerCallback should trigger for.
143      * @param executor The executor on which to trigger the observerCallback callback to deliver
144      *     notifications.
145      * @param observerCallback The callback to trigger on notifications.
146      */
registerObserverCallback( @onNull CallerAccess listeningPackageAccess, @NonNull String targetPackageName, @NonNull ObserverSpec spec, @NonNull Executor executor, @NonNull ObserverCallback observerCallback)147     public void registerObserverCallback(
148             @NonNull CallerAccess listeningPackageAccess,
149             @NonNull String targetPackageName,
150             @NonNull ObserverSpec spec,
151             @NonNull Executor executor,
152             @NonNull ObserverCallback observerCallback) {
153         synchronized (mLock) {
154             List<ObserverInfo> infos = mObserversLocked.get(targetPackageName);
155             if (infos == null) {
156                 infos = new ArrayList<>();
157                 mObserversLocked.put(targetPackageName, infos);
158             }
159             infos.add(new ObserverInfo(listeningPackageAccess, spec, executor, observerCallback));
160         }
161     }
162 
163     /**
164      * Removes all observers that match via {@link ObserverCallback#equals} to the given observer
165      * from watching the targetPackageName.
166      *
167      * <p>Pending notifications queued for this observer, if any, are discarded.
168      */
unregisterObserverCallback( @onNull String targetPackageName, @NonNull ObserverCallback observer)169     public void unregisterObserverCallback(
170             @NonNull String targetPackageName, @NonNull ObserverCallback observer) {
171         synchronized (mLock) {
172             List<ObserverInfo> infos = mObserversLocked.get(targetPackageName);
173             if (infos == null) {
174                 return;
175             }
176             for (int i = 0; i < infos.size(); i++) {
177                 if (infos.get(i).mObserverCallback.equals(observer)) {
178                     infos.remove(i);
179                     i--;
180                 }
181             }
182         }
183     }
184 
185     /**
186      * Should be called when a change occurs to a document.
187      *
188      * <p>The notification will be queued in memory for later dispatch. You must call {@link
189      * #dispatchAndClearPendingNotifications} to dispatch all such pending notifications.
190      *
191      * @param visibilityStore Store for visibility information. If not provided, only access to own
192      *     data will be allowed.
193      * @param visibilityChecker Checker for visibility access. If not provided, only access to own
194      *     data will be allowed.
195      */
onDocumentChange( @onNull String packageName, @NonNull String databaseName, @NonNull String namespace, @NonNull String schemaType, @NonNull String documentId, @Nullable VisibilityStore visibilityStore, @Nullable VisibilityChecker visibilityChecker)196     public void onDocumentChange(
197             @NonNull String packageName,
198             @NonNull String databaseName,
199             @NonNull String namespace,
200             @NonNull String schemaType,
201             @NonNull String documentId,
202             @Nullable VisibilityStore visibilityStore,
203             @Nullable VisibilityChecker visibilityChecker) {
204         synchronized (mLock) {
205             List<ObserverInfo> allObserverInfosForPackage = mObserversLocked.get(packageName);
206             if (allObserverInfosForPackage == null || allObserverInfosForPackage.isEmpty()) {
207                 return; // No observers for this type
208             }
209             // Enqueue changes for later dispatch once the call returns
210             String prefixedSchema = PrefixUtil.createPrefix(packageName, databaseName) + schemaType;
211             DocumentChangeGroupKey key = null;
212             for (int i = 0; i < allObserverInfosForPackage.size(); i++) {
213                 ObserverInfo observerInfo = allObserverInfosForPackage.get(i);
214                 if (!matchesSpec(schemaType, observerInfo.mObserverSpec)) {
215                     continue; // Observer doesn't want this notification
216                 }
217                 if (!VisibilityUtil.isSchemaSearchableByCaller(
218                         /*callerAccess=*/ observerInfo.mListeningPackageAccess,
219                         /*targetPackageName=*/ packageName,
220                         /*prefixedSchema=*/ prefixedSchema,
221                         visibilityStore,
222                         visibilityChecker)) {
223                     continue; // Observer can't have this notification.
224                 }
225                 if (key == null) {
226                     key =
227                             new DocumentChangeGroupKey(
228                                     packageName, databaseName, namespace, schemaType);
229                 }
230                 Set<String> changedDocumentIds = observerInfo.mDocumentChanges.get(key);
231                 if (changedDocumentIds == null) {
232                     changedDocumentIds = new ArraySet<>();
233                     observerInfo.mDocumentChanges.put(key, changedDocumentIds);
234                 }
235                 changedDocumentIds.add(documentId);
236             }
237             mHasNotifications = true;
238         }
239     }
240 
241     /**
242      * Enqueues a change to a schema type for a single observer.
243      *
244      * <p>The notification will be queued in memory for later dispatch. You must call {@link
245      * #dispatchAndClearPendingNotifications} to dispatch all such pending notifications.
246      *
247      * <p>Note that unlike {@link #onDocumentChange}, the changes reported here are not dropped for
248      * observers that don't have visibility. This is because the observer might have had visibility
249      * before the schema change, and a final deletion needs to be sent to it. Caller is responsible
250      * for checking visibility of these notifications.
251      *
252      * @param listeningPackageName Name of package that subscribed to notifications and has been
253      *     validated by the caller to have the right access to receive this notification.
254      * @param targetPackageName Name of package that owns the changed schema types.
255      * @param databaseName Database in which the changed schema types reside.
256      * @param schemaName Unprefixed name of the changed schema type.
257      */
onSchemaChange( @onNull String listeningPackageName, @NonNull String targetPackageName, @NonNull String databaseName, @NonNull String schemaName)258     public void onSchemaChange(
259             @NonNull String listeningPackageName,
260             @NonNull String targetPackageName,
261             @NonNull String databaseName,
262             @NonNull String schemaName) {
263         synchronized (mLock) {
264             List<ObserverInfo> allObserverInfosForPackage = mObserversLocked.get(targetPackageName);
265             if (allObserverInfosForPackage == null || allObserverInfosForPackage.isEmpty()) {
266                 return; // No observers for this type
267             }
268             // Enqueue changes for later dispatch once the call returns
269             String prefix = null;
270             for (int i = 0; i < allObserverInfosForPackage.size(); i++) {
271                 ObserverInfo observerInfo = allObserverInfosForPackage.get(i);
272                 if (!observerInfo
273                         .mListeningPackageAccess
274                         .getCallingPackageName()
275                         .equals(listeningPackageName)) {
276                     continue; // Not the observer we've been requested to update right now.
277                 }
278                 if (!matchesSpec(schemaName, observerInfo.mObserverSpec)) {
279                     continue; // Observer doesn't want this notification
280                 }
281                 if (prefix == null) {
282                     prefix = PrefixUtil.createPrefix(targetPackageName, databaseName);
283                 }
284                 Set<String> changedSchemaNames = observerInfo.mSchemaChanges.get(prefix);
285                 if (changedSchemaNames == null) {
286                     changedSchemaNames = new ArraySet<>();
287                     observerInfo.mSchemaChanges.put(prefix, changedSchemaNames);
288                 }
289                 changedSchemaNames.add(schemaName);
290             }
291             mHasNotifications = true;
292         }
293     }
294 
295     /** Returns whether there are any observers registered to watch the given package. */
isPackageObserved(@onNull String packageName)296     public boolean isPackageObserved(@NonNull String packageName) {
297         synchronized (mLock) {
298             return mObserversLocked.containsKey(packageName);
299         }
300     }
301 
302     /**
303      * Returns whether there are any observers registered to watch the given package and unprefixed
304      * schema type.
305      */
isSchemaTypeObserved(@onNull String packageName, @NonNull String schemaType)306     public boolean isSchemaTypeObserved(@NonNull String packageName, @NonNull String schemaType) {
307         synchronized (mLock) {
308             List<ObserverInfo> allObserverInfosForPackage = mObserversLocked.get(packageName);
309             if (allObserverInfosForPackage == null) {
310                 return false;
311             }
312             for (int i = 0; i < allObserverInfosForPackage.size(); i++) {
313                 ObserverInfo observerInfo = allObserverInfosForPackage.get(i);
314                 if (matchesSpec(schemaType, observerInfo.mObserverSpec)) {
315                     return true;
316                 }
317             }
318             return false;
319         }
320     }
321 
322     /**
323      * Returns package names of listening packages registered for changes on the given {@code
324      * packageName}, {@code databaseName} and unprefixed {@code schemaType}, only if they have
325      * access to that type according to the provided {@code visibilityChecker}.
326      */
327     @NonNull
getObserversForSchemaType( @onNull String packageName, @NonNull String databaseName, @NonNull String schemaType, @Nullable VisibilityStore visibilityStore, @Nullable VisibilityChecker visibilityChecker)328     public Set<String> getObserversForSchemaType(
329             @NonNull String packageName,
330             @NonNull String databaseName,
331             @NonNull String schemaType,
332             @Nullable VisibilityStore visibilityStore,
333             @Nullable VisibilityChecker visibilityChecker) {
334         synchronized (mLock) {
335             List<ObserverInfo> allObserverInfosForPackage = mObserversLocked.get(packageName);
336             if (allObserverInfosForPackage == null) {
337                 return Collections.emptySet();
338             }
339             Set<String> result = new ArraySet<>();
340             String prefixedSchema = PrefixUtil.createPrefix(packageName, databaseName) + schemaType;
341             for (int i = 0; i < allObserverInfosForPackage.size(); i++) {
342                 ObserverInfo observerInfo = allObserverInfosForPackage.get(i);
343                 if (!matchesSpec(schemaType, observerInfo.mObserverSpec)) {
344                     continue; // Observer doesn't want this notification
345                 }
346                 if (!VisibilityUtil.isSchemaSearchableByCaller(
347                         /*callerAccess=*/ observerInfo.mListeningPackageAccess,
348                         /*targetPackageName=*/ packageName,
349                         /*prefixedSchema=*/ prefixedSchema,
350                         visibilityStore,
351                         visibilityChecker)) {
352                     continue; // Observer can't have this notification.
353                 }
354                 result.add(observerInfo.mListeningPackageAccess.getCallingPackageName());
355             }
356             return result;
357         }
358     }
359 
360     /** Returns whether any notifications have been queued for dispatch. */
hasNotifications()361     public boolean hasNotifications() {
362         return mHasNotifications;
363     }
364 
365     /** Dispatches notifications on their corresponding executors. */
dispatchAndClearPendingNotifications()366     public void dispatchAndClearPendingNotifications() {
367         if (!mHasNotifications) {
368             return;
369         }
370         synchronized (mLock) {
371             if (mObserversLocked.isEmpty() || !mHasNotifications) {
372                 return;
373             }
374             for (List<ObserverInfo> observerInfos : mObserversLocked.values()) {
375                 for (int i = 0; i < observerInfos.size(); i++) {
376                     dispatchAndClearPendingNotificationsLocked(observerInfos.get(i));
377                 }
378             }
379             mHasNotifications = false;
380         }
381     }
382 
383     /** Dispatches pending notifications for the given observerInfo and clears the pending list. */
384     @GuardedBy("mLock")
dispatchAndClearPendingNotificationsLocked(@onNull ObserverInfo observerInfo)385     private void dispatchAndClearPendingNotificationsLocked(@NonNull ObserverInfo observerInfo) {
386         // Get and clear the pending changes
387         Map<String, Set<String>> schemaChanges = observerInfo.mSchemaChanges;
388         Map<DocumentChangeGroupKey, Set<String>> documentChanges = observerInfo.mDocumentChanges;
389         if (schemaChanges.isEmpty() && documentChanges.isEmpty()) {
390             return;
391         }
392         if (!schemaChanges.isEmpty()) {
393             observerInfo.mSchemaChanges = new ArrayMap<>();
394         }
395         if (!documentChanges.isEmpty()) {
396             observerInfo.mDocumentChanges = new ArrayMap<>();
397         }
398 
399         // Dispatch the pending changes
400         observerInfo.mExecutor.execute(
401                 () -> {
402                     // Schema changes
403                     if (!schemaChanges.isEmpty()) {
404                         for (Map.Entry<String, Set<String>> entry : schemaChanges.entrySet()) {
405                             SchemaChangeInfo schemaChangeInfo =
406                                     new SchemaChangeInfo(
407                                             /*packageName=*/ PrefixUtil.getPackageName(
408                                                     entry.getKey()),
409                                             /*databaseName=*/ PrefixUtil.getDatabaseName(
410                                                     entry.getKey()),
411                                             /*changedSchemaNames=*/ entry.getValue());
412 
413                             try {
414                                 observerInfo.mObserverCallback.onSchemaChanged(schemaChangeInfo);
415                             } catch (Throwable t) {
416                                 Log.w(TAG, "ObserverCallback threw exception during dispatch", t);
417                             }
418                         }
419                     }
420 
421                     // Document changes
422                     if (!documentChanges.isEmpty()) {
423                         for (Map.Entry<DocumentChangeGroupKey, Set<String>> entry :
424                                 documentChanges.entrySet()) {
425                             DocumentChangeInfo documentChangeInfo =
426                                     new DocumentChangeInfo(
427                                             entry.getKey().mPackageName,
428                                             entry.getKey().mDatabaseName,
429                                             entry.getKey().mNamespace,
430                                             entry.getKey().mSchemaName,
431                                             entry.getValue());
432 
433                             try {
434                                 observerInfo.mObserverCallback.onDocumentChanged(
435                                         documentChangeInfo);
436                             } catch (Throwable t) {
437                                 Log.w(TAG, "ObserverCallback threw exception during dispatch", t);
438                             }
439                         }
440                     }
441                 });
442     }
443 
444     /**
445      * Checks whether a change in the given {@code databaseName}, {@code namespace} and {@code
446      * schemaType} passes all the filters defined in the given {@code observerSpec}.
447      *
448      * <p>Note that this method does not check packageName; you must only use it to check
449      * observerSpecs which you know are observing the same package as the change.
450      */
matchesSpec( @onNull String schemaType, @NonNull ObserverSpec observerSpec)451     private static boolean matchesSpec(
452             @NonNull String schemaType, @NonNull ObserverSpec observerSpec) {
453         Set<String> schemaFilters = observerSpec.getFilterSchemas();
454         return schemaFilters.isEmpty() || schemaFilters.contains(schemaType);
455     }
456 }
457