• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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