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