1 /*
2  * Copyright 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package androidx.appsearch.platformstorage;
17 
18 import android.app.appsearch.AppSearchResult;
19 import android.app.appsearch.BatchResultCallback;
20 import android.content.Context;
21 import android.os.Build;
22 
23 import androidx.annotation.DoNotInline;
24 import androidx.annotation.GuardedBy;
25 import androidx.annotation.RequiresApi;
26 import androidx.annotation.RestrictTo;
27 import androidx.appsearch.app.AppSearchBatchResult;
28 import androidx.appsearch.app.Features;
29 import androidx.appsearch.app.GenericDocument;
30 import androidx.appsearch.app.GetByDocumentIdRequest;
31 import androidx.appsearch.app.GetSchemaResponse;
32 import androidx.appsearch.app.GlobalSearchSession;
33 import androidx.appsearch.app.ReportSystemUsageRequest;
34 import androidx.appsearch.app.SearchResults;
35 import androidx.appsearch.app.SearchSpec;
36 import androidx.appsearch.exceptions.AppSearchException;
37 import androidx.appsearch.observer.DocumentChangeInfo;
38 import androidx.appsearch.observer.ObserverCallback;
39 import androidx.appsearch.observer.ObserverSpec;
40 import androidx.appsearch.observer.SchemaChangeInfo;
41 import androidx.appsearch.platformstorage.converter.AppSearchResultToPlatformConverter;
42 import androidx.appsearch.platformstorage.converter.GenericDocumentToPlatformConverter;
43 import androidx.appsearch.platformstorage.converter.GetSchemaResponseToPlatformConverter;
44 import androidx.appsearch.platformstorage.converter.ObserverSpecToPlatformConverter;
45 import androidx.appsearch.platformstorage.converter.RequestToPlatformConverter;
46 import androidx.appsearch.platformstorage.converter.SearchSpecToPlatformConverter;
47 import androidx.appsearch.platformstorage.util.BatchResultCallbackAdapter;
48 import androidx.collection.ArrayMap;
49 import androidx.concurrent.futures.ResolvableFuture;
50 import androidx.core.util.Preconditions;
51 
52 import com.google.common.util.concurrent.ListenableFuture;
53 
54 import org.jspecify.annotations.NonNull;
55 
56 import java.util.Map;
57 import java.util.concurrent.Executor;
58 import java.util.function.Consumer;
59 
60 /**
61  * An implementation of {@link GlobalSearchSession} which proxies to a
62  * platform {@link android.app.appsearch.GlobalSearchSession}.
63  *
64  * @exportToFramework:hide
65  */
66 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
67 @RequiresApi(Build.VERSION_CODES.S)
68 class GlobalSearchSessionImpl implements GlobalSearchSession {
69     private final android.app.appsearch.GlobalSearchSession mPlatformSession;
70     private final Executor mExecutor;
71     private final Context mContext;
72     private final Features mFeatures;
73 
74     // Management of observer callbacks.
75     @GuardedBy("mObserverCallbacksLocked")
76     private final Map<ObserverCallback, android.app.appsearch.observer.ObserverCallback>
77             mObserverCallbacksLocked = new ArrayMap<>();
78 
GlobalSearchSessionImpl( android.app.appsearch.@onNull GlobalSearchSession platformSession, @NonNull Executor executor, @NonNull Context context)79     GlobalSearchSessionImpl(
80             android.app.appsearch.@NonNull GlobalSearchSession platformSession,
81             @NonNull Executor executor,
82             @NonNull Context context) {
83         mPlatformSession = Preconditions.checkNotNull(platformSession);
84         mExecutor = Preconditions.checkNotNull(executor);
85         mContext = Preconditions.checkNotNull(context);
86         mFeatures = new FeaturesImpl(mContext);
87     }
88 
89     @Override
90     public @NonNull ListenableFuture<AppSearchBatchResult<String, GenericDocument>>
getByDocumentIdAsync( @onNull String packageName, @NonNull String databaseName, @NonNull GetByDocumentIdRequest request)91             getByDocumentIdAsync(
92                     @NonNull String packageName, @NonNull String databaseName,
93                     @NonNull GetByDocumentIdRequest request) {
94         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
95             throw new UnsupportedOperationException(Features.GLOBAL_SEARCH_SESSION_GET_BY_ID
96                     + " is not supported on this AppSearch implementation.");
97         }
98         Preconditions.checkNotNull(packageName);
99         Preconditions.checkNotNull(databaseName);
100         Preconditions.checkNotNull(request);
101         ResolvableFuture<AppSearchBatchResult<String, GenericDocument>> future =
102                 ResolvableFuture.create();
103         ApiHelperForT.getByDocumentId(mPlatformSession, packageName, databaseName,
104                 RequestToPlatformConverter.toPlatformGetByDocumentIdRequest(request), mExecutor,
105                 new BatchResultCallbackAdapter<>(
106                         future, GenericDocumentToPlatformConverter::toJetpackGenericDocument));
107         return future;
108     }
109 
110     @Override
search( @onNull String queryExpression, @NonNull SearchSpec searchSpec)111     public @NonNull SearchResults search(
112             @NonNull String queryExpression,
113             @NonNull SearchSpec searchSpec) {
114         Preconditions.checkNotNull(queryExpression);
115         Preconditions.checkNotNull(searchSpec);
116         android.app.appsearch.SearchResults platformSearchResults =
117                 mPlatformSession.search(
118                         queryExpression,
119                         SearchSpecToPlatformConverter.toPlatformSearchSpec(mContext, searchSpec));
120         return new SearchResultsImpl(platformSearchResults, searchSpec, mExecutor, mContext);
121     }
122 
123     @Override
reportSystemUsageAsync( @onNull ReportSystemUsageRequest request)124     public @NonNull ListenableFuture<Void> reportSystemUsageAsync(
125             @NonNull ReportSystemUsageRequest request) {
126         Preconditions.checkNotNull(request);
127         ResolvableFuture<Void> future = ResolvableFuture.create();
128         mPlatformSession.reportSystemUsage(
129                 RequestToPlatformConverter.toPlatformReportSystemUsageRequest(request),
130                 mExecutor,
131                 result -> AppSearchResultToPlatformConverter.platformAppSearchResultToFuture(
132                         result, future));
133         return future;
134     }
135 
136     @Override
getSchemaAsync(@onNull String packageName, @NonNull String databaseName)137     public @NonNull ListenableFuture<GetSchemaResponse> getSchemaAsync(@NonNull String packageName,
138             @NonNull String databaseName) {
139         // Superclass is annotated with @RequiresFeature, so we shouldn't get here on an
140         // unsupported build.
141         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
142             throw new UnsupportedOperationException(
143                     Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA
144                             + " is not supported on this AppSearch implementation.");
145         }
146         ResolvableFuture<GetSchemaResponse> future = ResolvableFuture.create();
147         ApiHelperForT.getSchema(mPlatformSession, packageName, databaseName, mExecutor,
148                 result -> AppSearchResultToPlatformConverter.platformAppSearchResultToFuture(
149                         result,
150                         future,
151                         GetSchemaResponseToPlatformConverter::toJetpackGetSchemaResponse));
152         return future;
153     }
154 
155     @Override
getFeatures()156     public @NonNull Features getFeatures() {
157         return mFeatures;
158     }
159 
160     @Override
registerObserverCallback( @onNull String targetPackageName, @NonNull ObserverSpec spec, @NonNull Executor executor, @NonNull ObserverCallback observer)161     public void registerObserverCallback(
162             @NonNull String targetPackageName,
163             @NonNull ObserverSpec spec,
164             @NonNull Executor executor,
165             @NonNull ObserverCallback observer) throws AppSearchException {
166         Preconditions.checkNotNull(targetPackageName);
167         Preconditions.checkNotNull(spec);
168         Preconditions.checkNotNull(executor);
169         Preconditions.checkNotNull(observer);
170         // Superclass is annotated with @RequiresFeature, so we shouldn't get here on an
171         // unsupported build.
172         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
173             throw new UnsupportedOperationException(
174                     Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK
175                             + " is not supported on this AppSearch implementation");
176         }
177 
178         synchronized (mObserverCallbacksLocked) {
179             android.app.appsearch.observer.ObserverCallback frameworkCallback =
180                     mObserverCallbacksLocked.get(observer);
181             if (frameworkCallback == null) {
182                 // No stub is associated with this package and observer, so we must create one.
183                 frameworkCallback = new android.app.appsearch.observer.ObserverCallback() {
184                     @Override
185                     public void onSchemaChanged(
186                             android.app.appsearch.observer.@NonNull SchemaChangeInfo
187                                     platformSchemaChangeInfo) {
188                         SchemaChangeInfo jetpackSchemaChangeInfo =
189                                 ObserverSpecToPlatformConverter.toJetpackSchemaChangeInfo(
190                                         platformSchemaChangeInfo);
191                         observer.onSchemaChanged(jetpackSchemaChangeInfo);
192                     }
193 
194                     @Override
195                     public void onDocumentChanged(
196                             android.app.appsearch.observer.@NonNull DocumentChangeInfo
197                                     platformDocumentChangeInfo) {
198                         DocumentChangeInfo jetpackDocumentChangeInfo =
199                                 ObserverSpecToPlatformConverter.toJetpackDocumentChangeInfo(
200                                         platformDocumentChangeInfo);
201                         observer.onDocumentChanged(jetpackDocumentChangeInfo);
202                     }
203                 };
204             }
205 
206             // Regardless of whether this stub was fresh or not, we have to register it again
207             // because the user might be supplying a different spec.
208             try {
209                 ApiHelperForT.registerObserverCallback(mPlatformSession, targetPackageName,
210                         ObserverSpecToPlatformConverter.toPlatformObserverSpec(spec), executor,
211                         frameworkCallback);
212             } catch (android.app.appsearch.exceptions.AppSearchException e) {
213                 throw new AppSearchException((int) e.getResultCode(), e.getMessage(), e.getCause());
214             }
215 
216             // Now that registration has succeeded, save this stub into our in-memory cache. This
217             // isn't done when errors occur because the user may not call removeObserver if
218             // addObserver threw.
219             mObserverCallbacksLocked.put(observer, frameworkCallback);
220         }
221     }
222 
223     @Override
unregisterObserverCallback( @onNull String targetPackageName, @NonNull ObserverCallback observer)224     public void unregisterObserverCallback(
225             @NonNull String targetPackageName, @NonNull ObserverCallback observer)
226             throws AppSearchException {
227         Preconditions.checkNotNull(targetPackageName);
228         Preconditions.checkNotNull(observer);
229         // Superclass is annotated with @RequiresFeature, so we shouldn't get here on an
230         // unsupported build.
231         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
232             throw new UnsupportedOperationException(
233                     Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK
234                             + " is not supported on this AppSearch implementation");
235         }
236 
237         android.app.appsearch.observer.ObserverCallback frameworkCallback;
238         synchronized (mObserverCallbacksLocked) {
239             frameworkCallback = mObserverCallbacksLocked.get(observer);
240             if (frameworkCallback == null) {
241                 return;  // No such observer registered. Nothing to do.
242             }
243 
244             try {
245                 ApiHelperForT.unregisterObserverCallback(mPlatformSession, targetPackageName,
246                         frameworkCallback);
247             } catch (android.app.appsearch.exceptions.AppSearchException e) {
248                 throw new AppSearchException((int) e.getResultCode(), e.getMessage(), e.getCause());
249             }
250 
251             // Only remove from the in-memory map once removal from the service side succeeds
252             mObserverCallbacksLocked.remove(observer);
253         }
254     }
255 
256     @Override
close()257     public void close() {
258         mPlatformSession.close();
259     }
260 
261     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
262     private static class ApiHelperForT {
ApiHelperForT()263         private ApiHelperForT() {
264             // This class is not instantiable.
265         }
266 
267         @DoNotInline
getByDocumentId(android.app.appsearch.GlobalSearchSession platformSession, String packageName, String databaseName, android.app.appsearch.GetByDocumentIdRequest request, Executor executor, BatchResultCallback<String, android.app.appsearch.GenericDocument> callback)268         static void getByDocumentId(android.app.appsearch.GlobalSearchSession platformSession,
269                 String packageName, String databaseName,
270                 android.app.appsearch.GetByDocumentIdRequest request, Executor executor,
271                 BatchResultCallback<String, android.app.appsearch.GenericDocument> callback) {
272             platformSession.getByDocumentId(packageName, databaseName, request, executor, callback);
273         }
274 
275         @DoNotInline
getSchema(android.app.appsearch.GlobalSearchSession platformSessions, String packageName, String databaseName, Executor executor, Consumer<AppSearchResult<android.app.appsearch.GetSchemaResponse>> callback)276         static void getSchema(android.app.appsearch.GlobalSearchSession platformSessions,
277                 String packageName, String databaseName, Executor executor,
278                 Consumer<AppSearchResult<android.app.appsearch.GetSchemaResponse>> callback) {
279             platformSessions.getSchema(packageName, databaseName, executor, callback);
280         }
281 
282         @DoNotInline
registerObserverCallback( android.app.appsearch.GlobalSearchSession platformSession, String targetPackageName, android.app.appsearch.observer.ObserverSpec spec, Executor executor, android.app.appsearch.observer.ObserverCallback observer)283         static void registerObserverCallback(
284                 android.app.appsearch.GlobalSearchSession platformSession, String targetPackageName,
285                 android.app.appsearch.observer.ObserverSpec spec, Executor executor,
286                 android.app.appsearch.observer.ObserverCallback observer)
287                 throws android.app.appsearch.exceptions.AppSearchException {
288             platformSession.registerObserverCallback(targetPackageName, spec, executor, observer);
289         }
290 
291         @DoNotInline
unregisterObserverCallback( android.app.appsearch.GlobalSearchSession platformSession, String targetPackageName, android.app.appsearch.observer.ObserverCallback observer)292         static void unregisterObserverCallback(
293                 android.app.appsearch.GlobalSearchSession platformSession, String targetPackageName,
294                 android.app.appsearch.observer.ObserverCallback observer)
295                 throws android.app.appsearch.exceptions.AppSearchException {
296             platformSession.unregisterObserverCallback(targetPackageName, observer);
297         }
298     }
299 }
300