1 /* 2 * Copyright 2020 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 android.app.appsearch; 18 19 import static android.app.appsearch.SearchSessionUtil.safeExecute; 20 21 import android.annotation.CallbackExecutor; 22 import android.annotation.NonNull; 23 import android.app.appsearch.aidl.AppSearchResultParcel; 24 import android.app.appsearch.aidl.IAppSearchManager; 25 import android.app.appsearch.aidl.IAppSearchObserverProxy; 26 import android.app.appsearch.aidl.IAppSearchResultCallback; 27 import android.app.appsearch.exceptions.AppSearchException; 28 import android.app.appsearch.observer.DocumentChangeInfo; 29 import android.app.appsearch.observer.ObserverCallback; 30 import android.app.appsearch.observer.ObserverSpec; 31 import android.app.appsearch.observer.SchemaChangeInfo; 32 import android.content.AttributionSource; 33 import android.os.Bundle; 34 import android.os.RemoteException; 35 import android.os.SystemClock; 36 import android.os.UserHandle; 37 import android.util.ArrayMap; 38 import android.util.ArraySet; 39 import android.util.Log; 40 41 import com.android.internal.annotations.GuardedBy; 42 import com.android.internal.util.Preconditions; 43 44 import java.io.Closeable; 45 import java.util.ArrayList; 46 import java.util.List; 47 import java.util.Map; 48 import java.util.Objects; 49 import java.util.concurrent.Executor; 50 import java.util.function.Consumer; 51 52 /** 53 * Provides a connection to all AppSearch databases the querying application has been granted access 54 * to. 55 * 56 * <p>This class is thread safe. 57 * 58 * @see AppSearchSession 59 */ 60 public class GlobalSearchSession implements Closeable { 61 private static final String TAG = "AppSearchGlobalSearchSe"; 62 63 private final UserHandle mUserHandle; 64 private final IAppSearchManager mService; 65 private final AttributionSource mCallerAttributionSource; 66 67 // Management of observer callbacks. Key is observed package. 68 @GuardedBy("mObserverCallbacksLocked") 69 private final Map<String, Map<ObserverCallback, IAppSearchObserverProxy>> 70 mObserverCallbacksLocked = new ArrayMap<>(); 71 72 private boolean mIsMutated = false; 73 private boolean mIsClosed = false; 74 75 /** 76 * Creates a search session for the client, defined by the {@code userHandle} and 77 * {@code packageName}. 78 */ createGlobalSearchSession( @onNull IAppSearchManager service, @NonNull UserHandle userHandle, @NonNull AttributionSource attributionSource, @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<AppSearchResult<GlobalSearchSession>> callback)79 static void createGlobalSearchSession( 80 @NonNull IAppSearchManager service, 81 @NonNull UserHandle userHandle, 82 @NonNull AttributionSource attributionSource, 83 @NonNull @CallbackExecutor Executor executor, 84 @NonNull Consumer<AppSearchResult<GlobalSearchSession>> callback) { 85 GlobalSearchSession globalSearchSession = new GlobalSearchSession(service, userHandle, 86 attributionSource); 87 globalSearchSession.initialize(executor, callback); 88 } 89 90 // NOTE: No instance of this class should be created or returned except via initialize(). 91 // Once the callback.accept has been called here, the class is ready to use. initialize( @onNull @allbackExecutor Executor executor, @NonNull Consumer<AppSearchResult<GlobalSearchSession>> callback)92 private void initialize( 93 @NonNull @CallbackExecutor Executor executor, 94 @NonNull Consumer<AppSearchResult<GlobalSearchSession>> callback) { 95 try { 96 mService.initialize( 97 mCallerAttributionSource, 98 mUserHandle, 99 /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(), 100 new IAppSearchResultCallback.Stub() { 101 @Override 102 public void onResult(AppSearchResultParcel resultParcel) { 103 safeExecute(executor, callback, () -> { 104 AppSearchResult<Void> result = resultParcel.getResult(); 105 if (result.isSuccess()) { 106 callback.accept( 107 AppSearchResult.newSuccessfulResult( 108 GlobalSearchSession.this)); 109 } else { 110 callback.accept(AppSearchResult.newFailedResult(result)); 111 } 112 }); 113 } 114 }); 115 } catch (RemoteException e) { 116 throw e.rethrowFromSystemServer(); 117 } 118 } 119 GlobalSearchSession(@onNull IAppSearchManager service, @NonNull UserHandle userHandle, @NonNull AttributionSource callerAttributionSource)120 private GlobalSearchSession(@NonNull IAppSearchManager service, @NonNull UserHandle userHandle, 121 @NonNull AttributionSource callerAttributionSource) { 122 mService = service; 123 mUserHandle = userHandle; 124 mCallerAttributionSource = callerAttributionSource; 125 } 126 127 /** 128 * Retrieves {@link GenericDocument} documents, belonging to the specified package name and 129 * database name and identified by the namespace and ids in the request, from the 130 * {@link GlobalSearchSession} database. 131 * 132 * <p>If the package or database doesn't exist or if the calling package doesn't have access, 133 * the gets will be handled as failures in an {@link AppSearchBatchResult} object in the 134 * callback. 135 * 136 * @param packageName the name of the package to get from 137 * @param databaseName the name of the database to get from 138 * @param request a request containing a namespace and IDs to get documents for. 139 * @param executor Executor on which to invoke the callback. 140 * @param callback Callback to receive the pending result of performing this operation. The 141 * keys of the returned {@link AppSearchBatchResult} are the input IDs. The 142 * values are the returned {@link GenericDocument}s on success, or a failed 143 * {@link AppSearchResult} otherwise. IDs that are not found will return a 144 * failed {@link AppSearchResult} with a result code of 145 * {@link AppSearchResult#RESULT_NOT_FOUND}. If an unexpected internal error 146 * occurs in the AppSearch service, 147 * {@link BatchResultCallback#onSystemError} will be invoked with a 148 * {@link Throwable}. 149 */ getByDocumentId( @onNull String packageName, @NonNull String databaseName, @NonNull GetByDocumentIdRequest request, @NonNull @CallbackExecutor Executor executor, @NonNull BatchResultCallback<String, GenericDocument> callback)150 public void getByDocumentId( 151 @NonNull String packageName, 152 @NonNull String databaseName, 153 @NonNull GetByDocumentIdRequest request, 154 @NonNull @CallbackExecutor Executor executor, 155 @NonNull BatchResultCallback<String, GenericDocument> callback) { 156 Objects.requireNonNull(packageName); 157 Objects.requireNonNull(databaseName); 158 Objects.requireNonNull(request); 159 Objects.requireNonNull(executor); 160 Objects.requireNonNull(callback); 161 Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed"); 162 163 try { 164 mService.getDocuments( 165 mCallerAttributionSource, 166 /*targetPackageName=*/packageName, 167 databaseName, 168 request.getNamespace(), 169 new ArrayList<>(request.getIds()), 170 request.getProjectionsInternal(), 171 mUserHandle, 172 /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(), 173 SearchSessionUtil.createGetDocumentCallback(executor, callback)); 174 } catch (RemoteException e) { 175 throw e.rethrowFromSystemServer(); 176 } 177 } 178 179 /** 180 * Retrieves documents from all AppSearch databases that the querying application has access to. 181 * 182 * <p>Applications can be granted access to documents by specifying {@link 183 * SetSchemaRequest.Builder#setSchemaTypeVisibilityForPackage} when building a schema. 184 * 185 * <p>Document access can also be granted to system UIs by specifying {@link 186 * SetSchemaRequest.Builder#setSchemaTypeDisplayedBySystem} when building a schema. 187 * 188 * <p>See {@link AppSearchSession#search} for a detailed explanation on forming a query string. 189 * 190 * <p>This method is lightweight. The heavy work will be done in {@link 191 * SearchResults#getNextPage}. 192 * 193 * @param queryExpression query string to search. 194 * @param searchSpec spec for setting document filters, adding projection, setting term 195 * match type, etc. 196 * @return a {@link SearchResults} object for retrieved matched documents. 197 */ 198 @NonNull search(@onNull String queryExpression, @NonNull SearchSpec searchSpec)199 public SearchResults search(@NonNull String queryExpression, @NonNull SearchSpec searchSpec) { 200 Objects.requireNonNull(queryExpression); 201 Objects.requireNonNull(searchSpec); 202 Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed"); 203 return new SearchResults(mService, mCallerAttributionSource, /*databaseName=*/null, 204 queryExpression, searchSpec, mUserHandle); 205 } 206 207 /** 208 * Reports that a particular document has been used from a system surface. 209 * 210 * <p>See {@link AppSearchSession#reportUsage} for a general description of document usage, as 211 * well as an API that can be used by the app itself. 212 * 213 * <p>Usage reported via this method is accounted separately from usage reported via 214 * {@link AppSearchSession#reportUsage} and may be accessed using the constants 215 * {@link SearchSpec#RANKING_STRATEGY_SYSTEM_USAGE_COUNT} and 216 * {@link SearchSpec#RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP}. 217 * 218 * @param request The usage reporting request. 219 * @param executor Executor on which to invoke the callback. 220 * @param callback Callback to receive errors. If the operation succeeds, the callback will be 221 * invoked with an {@link AppSearchResult} whose value is {@code null}. The 222 * callback will be invoked with an {@link AppSearchResult} of 223 * {@link AppSearchResult#RESULT_SECURITY_ERROR} if this API is invoked by an 224 * app which is not part of the system. 225 */ reportSystemUsage( @onNull ReportSystemUsageRequest request, @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<AppSearchResult<Void>> callback)226 public void reportSystemUsage( 227 @NonNull ReportSystemUsageRequest request, 228 @NonNull @CallbackExecutor Executor executor, 229 @NonNull Consumer<AppSearchResult<Void>> callback) { 230 Objects.requireNonNull(request); 231 Objects.requireNonNull(executor); 232 Objects.requireNonNull(callback); 233 Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed"); 234 try { 235 mService.reportUsage( 236 mCallerAttributionSource, 237 request.getPackageName(), 238 request.getDatabaseName(), 239 request.getNamespace(), 240 request.getDocumentId(), 241 request.getUsageTimestampMillis(), 242 /*systemUsage=*/ true, 243 mUserHandle, 244 /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(), 245 new IAppSearchResultCallback.Stub() { 246 @Override 247 public void onResult(AppSearchResultParcel resultParcel) { 248 safeExecute( 249 executor, 250 callback, 251 () -> callback.accept(resultParcel.getResult())); 252 } 253 }); 254 mIsMutated = true; 255 } catch (RemoteException e) { 256 throw e.rethrowFromSystemServer(); 257 } 258 } 259 260 /** 261 * Retrieves the collection of schemas most recently successfully provided to {@link 262 * AppSearchSession#setSchema} for any types belonging to the requested package and database 263 * that the caller has been granted access to. 264 * 265 * <p>If the requested package/database combination does not exist or the caller has not been 266 * granted access to it, then an empty GetSchemaResponse will be returned. 267 * 268 * @param packageName the package that owns the requested {@link AppSearchSchema} instances. 269 * @param databaseName the database that owns the requested {@link AppSearchSchema} instances. 270 * @return The pending {@link GetSchemaResponse} containing the schemas that the caller has 271 * access to or an empty GetSchemaResponse if the request package and database does not 272 * exist, has not set a schema or contains no schemas that are accessible to the caller. 273 */ 274 // This call hits disk; async API prevents us from treating these calls as properties. getSchema( @onNull String packageName, @NonNull String databaseName, @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<AppSearchResult<GetSchemaResponse>> callback)275 public void getSchema( 276 @NonNull String packageName, 277 @NonNull String databaseName, 278 @NonNull @CallbackExecutor Executor executor, 279 @NonNull Consumer<AppSearchResult<GetSchemaResponse>> callback) { 280 Objects.requireNonNull(packageName); 281 Objects.requireNonNull(databaseName); 282 Objects.requireNonNull(executor); 283 Objects.requireNonNull(callback); 284 Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed"); 285 try { 286 mService.getSchema( 287 mCallerAttributionSource, 288 packageName, 289 databaseName, 290 mUserHandle, 291 /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(), 292 new IAppSearchResultCallback.Stub() { 293 @Override 294 public void onResult(AppSearchResultParcel resultParcel) { 295 safeExecute(executor, callback, () -> { 296 AppSearchResult<Bundle> result = resultParcel.getResult(); 297 if (result.isSuccess()) { 298 GetSchemaResponse response = new GetSchemaResponse( 299 Objects.requireNonNull(result.getResultValue())); 300 callback.accept(AppSearchResult.newSuccessfulResult(response)); 301 } else { 302 callback.accept(AppSearchResult.newFailedResult(result)); 303 } 304 }); 305 } 306 }); 307 } catch (RemoteException e) { 308 throw e.rethrowFromSystemServer(); 309 } 310 } 311 312 /** 313 * Adds an {@link ObserverCallback} to monitor changes within the databases owned by 314 * {@code targetPackageName} if they match the given 315 * {@link android.app.appsearch.observer.ObserverSpec}. 316 * 317 * <p>The observer callback is only triggered for data that changes after it is registered. No 318 * notification about existing data is sent as a result of registering an observer. To find out 319 * about existing data, you must use the {@link GlobalSearchSession#search} API. 320 * 321 * <p>If the data owned by {@code targetPackageName} is not visible to you, the registration 322 * call will succeed but no notifications will be dispatched. Notifications could start flowing 323 * later if {@code targetPackageName} changes its schema visibility settings. 324 * 325 * <p>If no package matching {@code targetPackageName} exists on the system, the registration 326 * call will succeed but no notifications will be dispatched. Notifications could start flowing 327 * later if {@code targetPackageName} is installed and starts indexing data. 328 * 329 * @param targetPackageName Package whose changes to monitor 330 * @param spec Specification of what types of changes to listen for 331 * @param executor Executor on which to call the {@code observer} callback methods. 332 * @param observer Callback to trigger when a schema or document changes 333 * @throws AppSearchException If an unexpected error occurs when trying to register an observer. 334 */ registerObserverCallback( @onNull String targetPackageName, @NonNull ObserverSpec spec, @NonNull Executor executor, @NonNull ObserverCallback observer)335 public void registerObserverCallback( 336 @NonNull String targetPackageName, 337 @NonNull ObserverSpec spec, 338 @NonNull Executor executor, 339 @NonNull ObserverCallback observer) throws AppSearchException { 340 Objects.requireNonNull(targetPackageName); 341 Objects.requireNonNull(spec); 342 Objects.requireNonNull(executor); 343 Objects.requireNonNull(observer); 344 Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed"); 345 346 synchronized (mObserverCallbacksLocked) { 347 IAppSearchObserverProxy stub = null; 348 Map<ObserverCallback, IAppSearchObserverProxy> observersForPackage = 349 mObserverCallbacksLocked.get(targetPackageName); 350 if (observersForPackage != null) { 351 stub = observersForPackage.get(observer); 352 } 353 if (stub == null) { 354 // No stub is associated with this package and observer, so we must create one. 355 stub = new IAppSearchObserverProxy.Stub() { 356 @Override 357 public void onSchemaChanged( 358 @NonNull String packageName, 359 @NonNull String databaseName, 360 @NonNull List<String> changedSchemaNames) { 361 safeExecute(executor, this::suppressingErrorCallback, () -> { 362 SchemaChangeInfo changeInfo = new SchemaChangeInfo( 363 packageName, databaseName, new ArraySet<>(changedSchemaNames)); 364 observer.onSchemaChanged(changeInfo); 365 }); 366 } 367 368 @Override 369 public void onDocumentChanged( 370 @NonNull String packageName, 371 @NonNull String databaseName, 372 @NonNull String namespace, 373 @NonNull String schemaName, 374 @NonNull List<String> changedDocumentIds) { 375 safeExecute(executor, this::suppressingErrorCallback, () -> { 376 DocumentChangeInfo changeInfo = new DocumentChangeInfo( 377 packageName, 378 databaseName, 379 namespace, 380 schemaName, 381 new ArraySet<>(changedDocumentIds)); 382 observer.onDocumentChanged(changeInfo); 383 }); 384 } 385 386 /** 387 * Error-handling callback that simply drops errors. 388 * 389 * <p>If we fail to deliver change notifications, there isn't much we can do. 390 * The API doesn't allow the user to provide a callback to invoke on failure of 391 * change notification delivery. {@link SearchSessionUtil#safeExecute} already 392 * includes a log message. So we just do nothing. 393 */ 394 private void suppressingErrorCallback(@NonNull AppSearchResult<?> unused) { 395 } 396 }; 397 } 398 399 // Regardless of whether this stub was fresh or not, we have to register it again 400 // because the user might be supplying a different spec. 401 AppSearchResultParcel<Void> resultParcel; 402 try { 403 resultParcel = mService.registerObserverCallback( 404 mCallerAttributionSource, 405 targetPackageName, 406 spec.getBundle(), 407 mUserHandle, 408 /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(), 409 stub); 410 } catch (RemoteException e) { 411 throw e.rethrowFromSystemServer(); 412 } 413 414 // See whether registration was successful 415 AppSearchResult<Void> result = resultParcel.getResult(); 416 if (!result.isSuccess()) { 417 throw new AppSearchException(result.getResultCode(), result.getErrorMessage()); 418 } 419 420 // Now that registration has succeeded, save this stub into our in-memory cache. This 421 // isn't done when errors occur because the user may not call unregisterObserverCallback 422 // if registerObserverCallback threw. 423 if (observersForPackage == null) { 424 observersForPackage = new ArrayMap<>(); 425 mObserverCallbacksLocked.put(targetPackageName, observersForPackage); 426 } 427 observersForPackage.put(observer, stub); 428 } 429 } 430 431 /** 432 * Removes previously registered {@link ObserverCallback} instances from the system. 433 * 434 * <p>All instances of {@link ObserverCallback} which are registered to observe 435 * {@code targetPackageName} and compare equal to the provided callback using the provided 436 * argument's {@code ObserverCallback#equals} will be removed. 437 * 438 * <p>If no matching observers have been registered, this method has no effect. If multiple 439 * matching observers have been registered, all will be removed. 440 * 441 * @param targetPackageName Package which the observers to be removed are listening to. 442 * @param observer Callback to unregister. 443 * @throws AppSearchException if an error occurs trying to remove the observer, such as a 444 * failure to communicate with the system service. Note that no error 445 * will be thrown if the provided observer doesn't match any 446 * registered observer. 447 */ unregisterObserverCallback( @onNull String targetPackageName, @NonNull ObserverCallback observer)448 public void unregisterObserverCallback( 449 @NonNull String targetPackageName, 450 @NonNull ObserverCallback observer) throws AppSearchException { 451 Objects.requireNonNull(targetPackageName); 452 Objects.requireNonNull(observer); 453 Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed"); 454 455 IAppSearchObserverProxy stub; 456 synchronized (mObserverCallbacksLocked) { 457 Map<ObserverCallback, IAppSearchObserverProxy> observersForPackage = 458 mObserverCallbacksLocked.get(targetPackageName); 459 if (observersForPackage == null) { 460 return; // No observers registered for this package. Nothing to do. 461 } 462 stub = observersForPackage.get(observer); 463 if (stub == null) { 464 return; // No such observer registered. Nothing to do. 465 } 466 467 AppSearchResultParcel<Void> resultParcel; 468 try { 469 resultParcel = mService.unregisterObserverCallback( 470 mCallerAttributionSource, 471 targetPackageName, 472 mUserHandle, 473 /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(), 474 stub); 475 } catch (RemoteException e) { 476 throw e.rethrowFromSystemServer(); 477 } 478 479 AppSearchResult<Void> result = resultParcel.getResult(); 480 if (!result.isSuccess()) { 481 throw new AppSearchException(result.getResultCode(), result.getErrorMessage()); 482 } 483 484 // Only remove from the in-memory map once removal from the service side succeeds 485 observersForPackage.remove(observer); 486 if (observersForPackage.isEmpty()) { 487 mObserverCallbacksLocked.remove(targetPackageName); 488 } 489 } 490 } 491 492 /** 493 * Closes the {@link GlobalSearchSession}. Persists all mutations, including usage reports, to 494 * disk. 495 */ 496 @Override close()497 public void close() { 498 if (mIsMutated && !mIsClosed) { 499 try { 500 mService.persistToDisk( 501 mCallerAttributionSource, 502 mUserHandle, 503 /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime()); 504 mIsClosed = true; 505 } catch (RemoteException e) { 506 Log.e(TAG, "Unable to close the GlobalSearchSession", e); 507 } 508 } 509 } 510 } 511