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