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