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