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